Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cef10b309 | ||
|
|
c3070be14a | ||
|
|
5570440eb1 | ||
|
|
ec0a5df5a1 | ||
|
|
8411b76ee5 | ||
|
|
822e8941ed | ||
|
|
7ac9bd8591 | ||
|
|
68a5784650 | ||
|
|
67f324b939 | ||
|
|
8db8c60e75 | ||
|
|
8e569a1d1f | ||
|
|
3caf8bc82b | ||
|
|
3da028415f | ||
|
|
104df1915d | ||
|
|
bfb6d44195 | ||
|
|
df0e8bc027 | ||
|
|
442b6ced35 | ||
|
|
111e11924f | ||
|
|
061cc69a6a | ||
|
|
f9950e1f01 | ||
|
|
895d259589 | ||
|
|
4ea80f34fa | ||
|
|
77878bf714 | ||
|
|
f85dde6323 | ||
|
|
6441f92c9f | ||
|
|
25b9fc8b6a | ||
|
|
090678776e | ||
|
|
9be6d443d7 | ||
|
|
678253d037 | ||
|
|
bd561fd191 | ||
|
|
38b5ee7314 | ||
|
|
11245462f0 | ||
|
|
351a5b87bf | ||
|
|
b780257098 | ||
|
|
4e1f1551ea | ||
|
|
b82e3f2a8a | ||
|
|
a82bf1bb32 | ||
|
|
abc0220cfa | ||
|
|
f17e6f9afd | ||
|
|
16e6b9eed7 | ||
|
|
323415ba9c | ||
|
|
ae97b5e704 | ||
|
|
6b8b30c3c7 | ||
|
|
0df2b2221d | ||
|
|
e2b36dfa7d | ||
|
|
4e18f24f3b | ||
|
|
b0d5a51768 | ||
|
|
b3d2c22373 | ||
|
|
cace88e8fa | ||
|
|
9c09d84c71 | ||
|
|
2d27665369 | ||
|
|
45266caa8d | ||
|
|
feb1a59902 | ||
|
|
fdec4157da | ||
|
|
4e84b20925 | ||
|
|
f952ad5913 | ||
|
|
be27586203 | ||
|
|
9dc3f3f38b | ||
|
|
f39defbe06 | ||
|
|
890f71a477 | ||
|
|
bc8e8c5daf | ||
|
|
37f12809a1 | ||
|
|
f5c0b847a9 | ||
|
|
44d6c3c07e | ||
|
|
da1a2b2957 | ||
|
|
9f6fa2bd05 | ||
|
|
5d68dc568f | ||
|
|
ee1ea881e8 | ||
|
|
87add88436 | ||
|
|
7643609e09 | ||
|
|
007a393ab5 | ||
|
|
80c90c0a00 | ||
|
|
c1c92647ca | ||
|
|
033adceb6f | ||
|
|
e57e92bfee | ||
|
|
a1a7729c3b | ||
|
|
071b0eeb77 | ||
|
|
fafc17c7d3 | ||
|
|
7599302920 | ||
|
|
7f8d7231a4 | ||
|
|
b1196885d7 | ||
|
|
494cfb3c04 | ||
|
|
6a65981103 | ||
|
|
f508f93d69 | ||
|
|
411d4434a3 | ||
|
|
d41fce6f91 | ||
|
|
282e7b4006 | ||
|
|
b4c3c5deea | ||
|
|
683514d891 | ||
|
|
e9beb21a98 | ||
|
|
bc47f78264 | ||
|
|
b002f7f862 | ||
|
|
05dac999a8 | ||
|
|
242595b725 | ||
|
|
48dd1a1aa6 | ||
|
|
3aacaffe6b | ||
|
|
61b875256f | ||
|
|
14dc450631 | ||
|
|
6352056528 | ||
|
|
bd4f24844b | ||
|
|
062615b6b1 | ||
|
|
6c9293e4f6 | ||
|
|
24802d64c7 | ||
|
|
5e8a686bb6 | ||
|
|
279ab89a61 | ||
|
|
29ed40051d | ||
|
|
8d05aa6262 | ||
|
|
694f942c06 | ||
|
|
105a2d4e13 | ||
|
|
1ee62912fd | ||
|
|
abacca34ee | ||
|
|
3e4e69735e | ||
|
|
4afc351933 | ||
|
|
23b8070b9d | ||
|
|
df435eb693 |
166
BaseClasses.py
@@ -10,6 +10,9 @@ from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple
|
||||
import secrets
|
||||
import random
|
||||
|
||||
import Options
|
||||
import Utils
|
||||
|
||||
|
||||
class MultiWorld():
|
||||
debug_types = False
|
||||
@@ -77,14 +80,10 @@ class MultiWorld():
|
||||
set_player_attr('shuffle', "vanilla")
|
||||
set_player_attr('logic', "noglitches")
|
||||
set_player_attr('mode', 'open')
|
||||
set_player_attr('swordless', False)
|
||||
set_player_attr('difficulty', 'normal')
|
||||
set_player_attr('item_functionality', 'normal')
|
||||
set_player_attr('timer', False)
|
||||
set_player_attr('goal', 'ganon')
|
||||
set_player_attr('accessibility', 'items')
|
||||
set_player_attr('retro', False)
|
||||
set_player_attr('hints', True)
|
||||
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
||||
set_player_attr('swamp_patch_required', False)
|
||||
set_player_attr('powder_patch_required', False)
|
||||
@@ -97,12 +96,8 @@ class MultiWorld():
|
||||
set_player_attr('fix_fake_world', True)
|
||||
set_player_attr('difficulty_requirements', None)
|
||||
set_player_attr('boss_shuffle', 'none')
|
||||
set_player_attr('enemy_shuffle', False)
|
||||
set_player_attr('enemy_health', 'default')
|
||||
set_player_attr('enemy_damage', 'default')
|
||||
set_player_attr('killable_thieves', False)
|
||||
set_player_attr('tile_shuffle', False)
|
||||
set_player_attr('bush_shuffle', False)
|
||||
set_player_attr('beemizer', 0)
|
||||
set_player_attr('escape_assist', [])
|
||||
set_player_attr('open_pyramid', False)
|
||||
@@ -114,16 +109,12 @@ 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('progression_balancing', True)
|
||||
set_player_attr('local_items', set())
|
||||
set_player_attr('non_local_items', set())
|
||||
set_player_attr('triforce_pieces_available', 30)
|
||||
set_player_attr('triforce_pieces_required', 20)
|
||||
set_player_attr('shop_shuffle', 'off')
|
||||
set_player_attr('shuffle_prizes', "g")
|
||||
set_player_attr('sprite_pool', [])
|
||||
set_player_attr('dark_room_logic', "lamp")
|
||||
set_player_attr('restrict_dungeon_item_on_boss', False)
|
||||
set_player_attr('plando_items', [])
|
||||
set_player_attr('plando_texts', {})
|
||||
set_player_attr('plando_connections', [])
|
||||
@@ -137,10 +128,21 @@ class MultiWorld():
|
||||
for player in self.player_ids:
|
||||
self.custom_data[player] = {}
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||
for option in world_type.options:
|
||||
setattr(self, option, getattr(args, option, {}))
|
||||
for option_key in world_type.options:
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
for option_key in Options.common_options:
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
for option_key in Options.per_game_common_options:
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
self.worlds[player] = world_type(self, player)
|
||||
|
||||
# intended for unittests
|
||||
def set_default_common_options(self):
|
||||
for option_key, option in Options.common_options.items():
|
||||
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
|
||||
for option_key, option in Options.per_game_common_options.items():
|
||||
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
|
||||
|
||||
def secure(self):
|
||||
self.random = secrets.SystemRandom()
|
||||
self.is_race = True
|
||||
@@ -390,17 +392,17 @@ class MultiWorld():
|
||||
"""Check if accessibility rules are fulfilled with current or supplied state."""
|
||||
if not state:
|
||||
state = CollectionState(self)
|
||||
players = {"none" : set(),
|
||||
players = {"minimal" : set(),
|
||||
"items": set(),
|
||||
"locations": set()}
|
||||
for player, access in self.accessibility.items():
|
||||
players[access].add(player)
|
||||
players[access.current_key].add(player)
|
||||
|
||||
beatable_fulfilled = False
|
||||
|
||||
def location_conditition(location : Location):
|
||||
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
||||
if location.player in players["none"]:
|
||||
if location.player in players["minimal"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -1009,10 +1011,8 @@ class Spoiler():
|
||||
self.medallions = {}
|
||||
self.playthrough = {}
|
||||
self.unreachables = []
|
||||
self.startinventory = []
|
||||
self.locations = {}
|
||||
self.paths = {}
|
||||
self.metadata = {}
|
||||
self.shops = []
|
||||
self.bosses = OrderedDict()
|
||||
|
||||
@@ -1028,8 +1028,6 @@ class Spoiler():
|
||||
self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][0]
|
||||
self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][1]
|
||||
|
||||
self.startinventory = list(map(str, self.world.precollected_items))
|
||||
|
||||
self.locations = OrderedDict()
|
||||
listed_locations = set()
|
||||
|
||||
@@ -1105,47 +1103,11 @@ class Spoiler():
|
||||
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
|
||||
self.bosses[str(player)]["Ganon"] = "Ganon"
|
||||
|
||||
from Utils import __version__ as APVersion
|
||||
self.metadata = {'version': APVersion,
|
||||
'logic': self.world.logic,
|
||||
'dark_room_logic': self.world.dark_room_logic,
|
||||
'mode': self.world.mode,
|
||||
'retro': self.world.retro,
|
||||
'swordless': self.world.swordless,
|
||||
'goal': self.world.goal,
|
||||
'shuffle': self.world.shuffle,
|
||||
'item_pool': self.world.difficulty,
|
||||
'item_functionality': self.world.item_functionality,
|
||||
'open_pyramid': self.world.open_pyramid,
|
||||
'accessibility': self.world.accessibility,
|
||||
'hints': self.world.hints,
|
||||
'boss_shuffle': self.world.boss_shuffle,
|
||||
'enemy_shuffle': self.world.enemy_shuffle,
|
||||
'enemy_health': self.world.enemy_health,
|
||||
'enemy_damage': self.world.enemy_damage,
|
||||
'killable_thieves': self.world.killable_thieves,
|
||||
'tile_shuffle': self.world.tile_shuffle,
|
||||
'bush_shuffle': self.world.bush_shuffle,
|
||||
'beemizer': self.world.beemizer,
|
||||
'shufflepots': self.world.shufflepots,
|
||||
'players': self.world.players,
|
||||
'progression_balancing': self.world.progression_balancing,
|
||||
'triforce_pieces_available': self.world.triforce_pieces_available,
|
||||
'triforce_pieces_required': self.world.triforce_pieces_required,
|
||||
'shop_shuffle': self.world.shop_shuffle,
|
||||
'shuffle_prizes': self.world.shuffle_prizes,
|
||||
'sprite_pool': self.world.sprite_pool,
|
||||
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
|
||||
'game': self.world.game,
|
||||
'er_seeds': self.world.er_seeds
|
||||
}
|
||||
|
||||
def to_json(self):
|
||||
self.parse_data()
|
||||
out = OrderedDict()
|
||||
out['Entrances'] = list(self.entrances.values())
|
||||
out.update(self.locations)
|
||||
out['Starting Inventory'] = self.startinventory
|
||||
out['Special'] = self.medallions
|
||||
if self.hashes:
|
||||
out['Hashes'] = self.hashes
|
||||
@@ -1154,7 +1116,6 @@ class Spoiler():
|
||||
out['playthrough'] = self.playthrough
|
||||
out['paths'] = self.paths
|
||||
out['Bosses'] = self.bosses
|
||||
out['meta'] = self.metadata
|
||||
|
||||
return json.dumps(out)
|
||||
|
||||
@@ -1166,10 +1127,18 @@ class Spoiler():
|
||||
return variable
|
||||
return 'Yes' if variable else 'No'
|
||||
|
||||
def write_option(option_key: str, option_obj: type(Options.Option)):
|
||||
res = getattr(self.world, option_key)[player]
|
||||
displayname = getattr(option_obj, "displayname", option_key)
|
||||
try:
|
||||
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
|
||||
except:
|
||||
raise Exception
|
||||
|
||||
with open(filename, 'w', encoding="utf-8-sig") as outfile:
|
||||
outfile.write(
|
||||
'Archipelago Version %s - Seed: %s\n\n' % (
|
||||
self.metadata['version'], self.world.seed))
|
||||
Utils.__version__, self.world.seed))
|
||||
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
|
||||
outfile.write('Players: %d\n' % self.world.players)
|
||||
|
||||
@@ -1177,68 +1146,51 @@ class Spoiler():
|
||||
if self.world.players > 1:
|
||||
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player)))
|
||||
outfile.write('Game: %s\n' % self.world.game[player])
|
||||
if self.world.players > 1:
|
||||
outfile.write('Progression Balanced: %s\n' % (
|
||||
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
|
||||
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
||||
for f_option, option in Options.common_options.items():
|
||||
write_option(f_option, option)
|
||||
for f_option, option in Options.per_game_common_options.items():
|
||||
write_option(f_option, option)
|
||||
options = self.world.worlds[player].options
|
||||
if options:
|
||||
for f_option, option in options.items():
|
||||
res = getattr(self.world, f_option)[player]
|
||||
displayname = getattr(option, "displayname", f_option)
|
||||
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
|
||||
write_option(f_option, option)
|
||||
|
||||
if player in self.world.get_game_players("A Link to the Past"):
|
||||
outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
|
||||
|
||||
outfile.write('Logic: %s\n' % self.metadata['logic'][player])
|
||||
outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player])
|
||||
outfile.write('Restricted Boss Drops: %s\n' %
|
||||
bool_to_text(self.metadata['restrict_dungeon_item_on_boss'][player]))
|
||||
|
||||
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
|
||||
outfile.write('Retro: %s\n' %
|
||||
('Yes' if self.metadata['retro'][player] else 'No'))
|
||||
outfile.write('Swordless: %s\n' % ('Yes' if self.metadata['swordless'][player] else 'No'))
|
||||
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
|
||||
if "triforce" in self.metadata["goal"][player]: # triforce hunt
|
||||
outfile.write('Logic: %s\n' % self.world.logic[player])
|
||||
outfile.write('Dark Room Logic: %s\n' % self.world.dark_room_logic[player])
|
||||
outfile.write('Mode: %s\n' % self.world.mode[player])
|
||||
outfile.write('Goal: %s\n' % self.world.goal[player])
|
||||
if "triforce" in self.world.goal[player]: # triforce hunt
|
||||
outfile.write("Pieces available for Triforce: %s\n" %
|
||||
self.metadata['triforce_pieces_available'][player])
|
||||
self.world.triforce_pieces_available[player])
|
||||
outfile.write("Pieces required for Triforce: %s\n" %
|
||||
self.metadata["triforce_pieces_required"][player])
|
||||
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
|
||||
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
|
||||
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
||||
if self.metadata['shuffle'][player] != "vanilla":
|
||||
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
|
||||
self.world.triforce_pieces_required[player])
|
||||
outfile.write('Difficulty: %s\n' % self.world.difficulty[player])
|
||||
outfile.write('Item Functionality: %s\n' % self.world.item_functionality[player])
|
||||
outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player])
|
||||
if self.world.shuffle[player] != "vanilla":
|
||||
outfile.write('Entrance Shuffle Seed %s\n' % self.world.er_seeds[player])
|
||||
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
||||
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
||||
'Yes' if self.world.open_pyramid[player] else 'No'))
|
||||
outfile.write('Shop inventory shuffle: %s\n' %
|
||||
bool_to_text("i" in self.metadata["shop_shuffle"][player]))
|
||||
bool_to_text("i" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Shop price shuffle: %s\n' %
|
||||
bool_to_text("p" in self.metadata["shop_shuffle"][player]))
|
||||
bool_to_text("p" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Shop upgrade shuffle: %s\n' %
|
||||
bool_to_text("u" in self.metadata["shop_shuffle"][player]))
|
||||
bool_to_text("u" in self.world.shop_shuffle[player]))
|
||||
outfile.write('New Shop inventory: %s\n' %
|
||||
bool_to_text("g" in self.metadata["shop_shuffle"][player] or
|
||||
"f" in self.metadata["shop_shuffle"][player]))
|
||||
bool_to_text("g" in self.world.shop_shuffle[player] or
|
||||
"f" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Custom Potion Shop: %s\n' %
|
||||
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
|
||||
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
|
||||
outfile.write(
|
||||
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
|
||||
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
|
||||
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
|
||||
outfile.write(f'Killable thieves: {bool_to_text(self.metadata["killable_thieves"][player])}\n')
|
||||
outfile.write(f'Shuffled tiles: {bool_to_text(self.metadata["tile_shuffle"][player])}\n')
|
||||
outfile.write(f'Shuffled bushes: {bool_to_text(self.metadata["bush_shuffle"][player])}\n')
|
||||
outfile.write(
|
||||
'Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
|
||||
outfile.write('Beemizer: %s\n' % self.metadata['beemizer'][player])
|
||||
outfile.write('Pot shuffle %s\n'
|
||||
% ('Yes' if self.metadata['shufflepots'][player] else 'No'))
|
||||
bool_to_text("w" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Boss shuffle: %s\n' % self.world.boss_shuffle[player])
|
||||
outfile.write('Enemy health: %s\n' % self.world.enemy_health[player])
|
||||
outfile.write('Enemy damage: %s\n' % self.world.enemy_damage[player])
|
||||
outfile.write('Beemizer: %s\n' % self.world.beemizer[player])
|
||||
outfile.write('Prize shuffle %s\n' %
|
||||
self.metadata['shuffle_prizes'][player])
|
||||
self.world.shuffle_prizes[player])
|
||||
if self.entrances:
|
||||
outfile.write('\n\nEntrances:\n\n')
|
||||
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_name(entry["player"])}: '
|
||||
@@ -1259,10 +1211,6 @@ class Spoiler():
|
||||
for recipe in self.world.worlds[player].custom_recipes.values():
|
||||
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
|
||||
|
||||
if self.startinventory:
|
||||
outfile.write('\n\nStarting Inventory:\n\n')
|
||||
outfile.write('\n'.join(self.startinventory))
|
||||
|
||||
outfile.write('\n\nLocations:\n\n')
|
||||
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ logger = logging.getLogger("Client")
|
||||
|
||||
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
|
||||
|
||||
|
||||
class ClientCommandProcessor(CommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
self.ctx = ctx
|
||||
@@ -86,11 +87,12 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
|
||||
|
||||
class CommonContext():
|
||||
starting_reconnect_delay = 5
|
||||
current_reconnect_delay = starting_reconnect_delay
|
||||
command_processor = ClientCommandProcessor
|
||||
game: None
|
||||
ui: None
|
||||
starting_reconnect_delay: int = 5
|
||||
current_reconnect_delay: int = starting_reconnect_delay
|
||||
command_processor: int = ClientCommandProcessor
|
||||
game = None
|
||||
ui = None
|
||||
keep_alive_task = None
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
# server state
|
||||
@@ -127,6 +129,9 @@ class CommonContext():
|
||||
self.jsontotextparser = JSONtoTextParser(self)
|
||||
self.set_getters(network_data_package)
|
||||
|
||||
# execution
|
||||
self.keep_alive_task = asyncio.create_task(keep_alive(self))
|
||||
|
||||
async def connection_closed(self):
|
||||
self.auth = None
|
||||
self.items_received = []
|
||||
@@ -190,6 +195,9 @@ class CommonContext():
|
||||
def event_invalid_slot(self):
|
||||
raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
|
||||
|
||||
def event_invalid_game(self):
|
||||
raise Exception('Invalid Game; please verify that you connected with the right game to the correct world.')
|
||||
|
||||
async def server_auth(self, password_requested):
|
||||
if password_requested and not self.password:
|
||||
logger.info('Enter the password required to join this game:')
|
||||
@@ -219,6 +227,19 @@ class CommonContext():
|
||||
pass
|
||||
|
||||
|
||||
async def keep_alive(ctx: CommonContext, seconds_between_checks=100):
|
||||
"""some ISPs/network configurations drop TCP connections if no payload is sent (ignore TCP-keep-alive)
|
||||
so we send a payload to prevent drop and if we were dropped anyway this will cause an auto-reconnect."""
|
||||
seconds_elapsed = 0
|
||||
while not ctx.exit_event.is_set():
|
||||
await asyncio.sleep(1) # short sleep to not block program shutdown
|
||||
if ctx.server and ctx.slot:
|
||||
seconds_elapsed += 1
|
||||
if seconds_elapsed > seconds_between_checks:
|
||||
await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot]}])
|
||||
seconds_elapsed = 0
|
||||
|
||||
|
||||
async def server_loop(ctx: CommonContext, address=None):
|
||||
cached_address = None
|
||||
if ctx.server and ctx.server.socket:
|
||||
@@ -252,13 +273,13 @@ async def server_loop(ctx: CommonContext, address=None):
|
||||
logger.error('Unable to connect to multiworld server at cached address. '
|
||||
'Please use the connect button above.')
|
||||
else:
|
||||
logger.error('Connection refused by the multiworld server')
|
||||
logger.exception('Connection refused by the multiworld server')
|
||||
except websockets.InvalidURI:
|
||||
logger.exception('Failed to connect to the multiworld server (invalid URI)')
|
||||
except (OSError, websockets.InvalidURI):
|
||||
logger.error('Failed to connect to the multiworld server')
|
||||
logger.exception('Failed to connect to the multiworld server')
|
||||
except Exception as e:
|
||||
logger.error('Lost connection to the multiworld server, type /connect to reconnect')
|
||||
if not isinstance(e, websockets.WebSocketException):
|
||||
logger.exception(e)
|
||||
logger.exception('Lost connection to the multiworld server, type /connect to reconnect')
|
||||
finally:
|
||||
await ctx.connection_closed()
|
||||
if ctx.server_address:
|
||||
@@ -327,7 +348,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
errors = args["errors"]
|
||||
if 'InvalidSlot' in errors:
|
||||
ctx.event_invalid_slot()
|
||||
|
||||
elif 'InvalidGame' in errors:
|
||||
ctx.event_invalid_game()
|
||||
elif 'SlotAlreadyTaken' in errors:
|
||||
raise Exception('Player slot already in use for that team')
|
||||
elif 'IncompatibleVersion' in errors:
|
||||
|
||||
@@ -239,7 +239,7 @@ def get_info(ctx, rcon_client):
|
||||
ctx.seed_name = info["seed_name"]
|
||||
|
||||
|
||||
async def factorio_spinup_server(ctx: FactorioContext):
|
||||
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||
savegame_name = os.path.abspath("Archipelago.zip")
|
||||
if not os.path.exists(savegame_name):
|
||||
logger.info(f"Creating savegame {savegame_name}")
|
||||
@@ -267,24 +267,23 @@ async def factorio_spinup_server(ctx: FactorioContext):
|
||||
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
|
||||
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||
if ctx.mod_version == ctx.__class__.mod_version:
|
||||
raise Exception("No Archipelago mod was loaded. Aborting.")
|
||||
get_info(ctx, rcon_client)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
if ctx.mod_version == ctx.__class__.mod_version:
|
||||
raise Exception("No Archipelago mod was loaded. Aborting.")
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error("Aborted Factorio Server Bridge")
|
||||
logger.exception(e)
|
||||
logger.error("Aborted Factorio Server Bridge")
|
||||
ctx.exit_event.set()
|
||||
|
||||
else:
|
||||
logger.info(f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
|
||||
|
||||
return True
|
||||
finally:
|
||||
factorio_process.terminate()
|
||||
factorio_process.wait(5)
|
||||
|
||||
return False
|
||||
|
||||
async def main(args):
|
||||
ctx = FactorioContext(args.connect, args.password)
|
||||
@@ -298,16 +297,17 @@ async def main(args):
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
ui_task = None
|
||||
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
|
||||
await factorio_server_task
|
||||
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
||||
progression_watcher = asyncio.create_task(
|
||||
game_watcher(ctx), name="FactorioProgressionWatcher")
|
||||
succesful_launch = await factorio_server_task
|
||||
if succesful_launch:
|
||||
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
||||
progression_watcher = asyncio.create_task(
|
||||
game_watcher(ctx), name="FactorioProgressionWatcher")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await progression_watcher
|
||||
await factorio_server_task
|
||||
await progression_watcher
|
||||
await factorio_server_task
|
||||
|
||||
if ctx.server and not ctx.server.socket.closed:
|
||||
await ctx.server.socket.close()
|
||||
|
||||
8
Fill.py
@@ -36,7 +36,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
for item_to_place in items_to_place:
|
||||
if world.accessibility[item_to_place.player] == 'none':
|
||||
if world.accessibility[item_to_place.player] == 'minimal':
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
|
||||
item_to_place.player) if single_player_placement else not has_beaten_game
|
||||
else:
|
||||
@@ -52,7 +52,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
|
||||
else:
|
||||
# we filled all reachable spots. Maybe the game can be beaten anyway?
|
||||
unplaced_items.append(item_to_place)
|
||||
if world.accessibility[item_to_place.player] != 'none' and world.can_beat_game():
|
||||
if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game():
|
||||
logging.warning(
|
||||
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
|
||||
continue
|
||||
@@ -87,9 +87,9 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
||||
progitempool.append(item)
|
||||
elif item.never_exclude: # this only gets nonprogression items which should not appear in excluded locations
|
||||
nonexcludeditempool.append(item)
|
||||
elif item.name in world.local_items[item.player]:
|
||||
elif item.name in world.local_items[item.player].value:
|
||||
localrestitempool[item.player].append(item)
|
||||
elif item.name in world.non_local_items[item.player]:
|
||||
elif item.name in world.non_local_items[item.player].value:
|
||||
nonlocalrestitempool.append(item)
|
||||
else:
|
||||
restitempool.append(item)
|
||||
|
||||
184
Generate.py
@@ -19,10 +19,8 @@ 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_table
|
||||
from worlds.alttp import Bosses
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.alttp.Regions import location_table, key_drop_data
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
categories = set(AutoWorldRegister.world_types)
|
||||
@@ -113,17 +111,17 @@ def main(args=None, callback=ERmain):
|
||||
player_id += 1
|
||||
|
||||
args.multi = max(player_id-1, args.multi)
|
||||
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed}")
|
||||
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
|
||||
f"{', '.join(args.plando)}")
|
||||
|
||||
if not weights_cache:
|
||||
raise Exception(f"No weights found. Provide a general weights file ({args.weights_file_path}) or individual player files. "
|
||||
f"A mix is also permitted.")
|
||||
erargs = parse_arguments(['--multi', str(args.multi)])
|
||||
erargs.seed = seed
|
||||
erargs.create_spoiler = args.spoiler > 0
|
||||
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
|
||||
erargs.spoiler = args.spoiler
|
||||
erargs.race = args.race
|
||||
erargs.skip_playthrough = args.spoiler < 2
|
||||
erargs.outputname = seed_name
|
||||
erargs.outputpath = args.outputpath
|
||||
|
||||
@@ -426,6 +424,32 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
|
||||
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
|
||||
|
||||
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option)):
|
||||
if option_key in game_weights:
|
||||
try:
|
||||
if not option.supports_weighting:
|
||||
player_option = option.from_any(game_weights[option_key])
|
||||
else:
|
||||
player_option = option.from_any(get_choice(option_key, game_weights))
|
||||
setattr(ret, option_key, player_option)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
|
||||
else:
|
||||
# verify item names existing
|
||||
if getattr(player_option, "verify_item_name", False):
|
||||
for item_name in player_option.value:
|
||||
if item_name not in AutoWorldRegister.world_types[ret.game].item_names:
|
||||
raise Exception(f"Item {item_name} from option {player_option} "
|
||||
f"is not a valid item name from {ret.game}")
|
||||
elif getattr(player_option, "verify_location_name", False):
|
||||
for location_name in player_option.value:
|
||||
if location_name not in AutoWorldRegister.world_types[ret.game].location_names:
|
||||
raise Exception(f"Location {location_name} from option {player_option} "
|
||||
f"is not a valid location name from {ret.game}")
|
||||
else:
|
||||
setattr(ret, option_key, option(option.default))
|
||||
|
||||
|
||||
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
|
||||
if "linked_options" in weights:
|
||||
weights = roll_linked_options(weights)
|
||||
@@ -452,63 +476,26 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
f"which are not enabled.")
|
||||
|
||||
ret = argparse.Namespace()
|
||||
for option_key in Options.per_game_common_options:
|
||||
if option_key in weights:
|
||||
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
|
||||
|
||||
ret.name = get_choice('name', weights)
|
||||
ret.accessibility = get_choice('accessibility', weights)
|
||||
ret.progression_balancing = get_choice('progression_balancing', weights, True)
|
||||
for option_key, option in Options.common_options.items():
|
||||
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
|
||||
ret.game = get_choice("game", weights)
|
||||
if ret.game not in weights:
|
||||
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
|
||||
world_type = AutoWorldRegister.world_types[ret.game]
|
||||
game_weights = weights[ret.game]
|
||||
ret.local_items = set()
|
||||
for item_name in game_weights.get('local_items', []):
|
||||
items = world_type.item_name_groups.get(item_name, {item_name})
|
||||
for item in items:
|
||||
if item in world_type.item_names:
|
||||
ret.local_items.add(item)
|
||||
else:
|
||||
raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.")
|
||||
|
||||
ret.non_local_items = set()
|
||||
for item_name in game_weights.get('non_local_items', []):
|
||||
items = world_type.item_name_groups.get(item_name, {item_name})
|
||||
for item in items:
|
||||
if item in world_type.item_names:
|
||||
ret.non_local_items.add(item)
|
||||
else:
|
||||
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
|
||||
|
||||
inventoryweights = game_weights.get('start_inventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
itemvalue = get_choice_legacy(item, inventoryweights)
|
||||
if isinstance(itemvalue, int):
|
||||
for i in range(int(itemvalue)):
|
||||
startitems.append(item)
|
||||
elif itemvalue:
|
||||
startitems.append(item)
|
||||
ret.startinventory = startitems
|
||||
ret.start_hints = set(game_weights.get('start_hints', []))
|
||||
|
||||
ret.excluded_locations = set()
|
||||
for location in game_weights.get('exclude_locations', []):
|
||||
if location in world_type.location_names:
|
||||
ret.excluded_locations.add(location)
|
||||
else:
|
||||
raise Exception(f"Could not exclude location {location}, as it was not recognized.")
|
||||
|
||||
if ret.game in AutoWorldRegister.world_types:
|
||||
for option_name, option in AutoWorldRegister.world_types[ret.game].options.items():
|
||||
if option_name in game_weights:
|
||||
try:
|
||||
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)))
|
||||
except Exception as e:
|
||||
raise Exception(f"Error generating option {option_name} in {ret.game}") from e
|
||||
else:
|
||||
setattr(ret, option_name, option(option.default))
|
||||
for option_key, option in world_type.options.items():
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
for option_key, option in Options.per_game_common_options.items():
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
if "items" in plando_options:
|
||||
ret.plando_items = roll_item_plando(world_type, game_weights)
|
||||
if ret.game == "Minecraft":
|
||||
# bad hardcoded behavior to make this work for now
|
||||
ret.plando_connections = []
|
||||
@@ -528,6 +515,44 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
return ret
|
||||
|
||||
|
||||
def roll_item_plando(world_type, weights):
|
||||
plando_items = []
|
||||
|
||||
def add_plando_item(item: str, location: str):
|
||||
if item not in world_type.item_name_to_id:
|
||||
raise Exception(f"Could not plando item {item} as the item was not recognized")
|
||||
if location not in world_type.location_name_to_id:
|
||||
raise Exception(
|
||||
f"Could not plando item {item} at location {location} as the location was not recognized")
|
||||
plando_items.append(PlandoItem(item, location, location_world, from_pool, force))
|
||||
|
||||
options = weights.get("plando_items", [])
|
||||
for placement in options:
|
||||
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"]
|
||||
if isinstance(items, dict):
|
||||
item_list = []
|
||||
for key, value in items.items():
|
||||
item_list += [key] * value
|
||||
items = item_list
|
||||
if not items or not locations:
|
||||
raise Exception("You must specify at least one item and one location to place items.")
|
||||
random.shuffle(items)
|
||||
random.shuffle(locations)
|
||||
for item, location in zip(items, locations):
|
||||
add_plando_item(item, location)
|
||||
else:
|
||||
item = get_choice_legacy("item", placement, get_choice_legacy("items", placement))
|
||||
location = get_choice_legacy("location", placement)
|
||||
add_plando_item(item, location)
|
||||
return plando_items
|
||||
|
||||
|
||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
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)}.")
|
||||
@@ -547,8 +572,6 @@ 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_legacy('restrict_dungeon_item_on_boss', weights, False)
|
||||
|
||||
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
|
||||
if entrance_shuffle.startswith('none-'):
|
||||
ret.shuffle = 'vanilla'
|
||||
@@ -588,11 +611,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
ret.shop_shuffle = ''
|
||||
|
||||
ret.mode = get_choice_legacy("mode", weights)
|
||||
ret.retro = get_choice_legacy("retro", weights)
|
||||
|
||||
ret.hints = get_choice_legacy('hints', weights)
|
||||
|
||||
ret.swordless = get_choice_legacy('swordless', weights, False)
|
||||
|
||||
ret.difficulty = get_choice_legacy('item_pool', weights)
|
||||
|
||||
@@ -601,12 +619,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
boss_shuffle = get_choice_legacy('boss_shuffle', weights)
|
||||
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
|
||||
|
||||
ret.enemy_shuffle = bool(get_choice_legacy('enemy_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',
|
||||
@@ -616,8 +628,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
|
||||
ret.enemy_health = get_choice_legacy('enemy_health', weights)
|
||||
|
||||
ret.shufflepots = get_choice_legacy('pot_shuffle', weights)
|
||||
|
||||
ret.beemizer = int(get_choice_legacy('beemizer', weights, 0))
|
||||
|
||||
ret.timer = {'none': False,
|
||||
@@ -647,42 +657,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.plando_items = []
|
||||
if "items" in plando_options:
|
||||
|
||||
def add_plando_item(item: str, location: str):
|
||||
if item not in item_table:
|
||||
raise Exception(f"Could not plando item {item} as the item was not recognized")
|
||||
if location not in location_table and location not in key_drop_data:
|
||||
raise Exception(
|
||||
f"Could not plando item {item} at location {location} as the location was not recognized")
|
||||
ret.plando_items.append(PlandoItem(item, location, location_world, from_pool, force))
|
||||
|
||||
options = weights.get("plando_items", [])
|
||||
for placement in options:
|
||||
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"]
|
||||
if isinstance(items, dict):
|
||||
item_list = []
|
||||
for key, value in items.items():
|
||||
item_list += [key] * value
|
||||
items = item_list
|
||||
if not items or not locations:
|
||||
raise Exception("You must specify at least one item and one location to place items.")
|
||||
random.shuffle(items)
|
||||
random.shuffle(locations)
|
||||
for item, location in zip(items, locations):
|
||||
add_plando_item(item, location)
|
||||
else:
|
||||
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 = {}
|
||||
if "texts" in plando_options:
|
||||
tt = TextTable()
|
||||
|
||||
49
Main.py
@@ -50,42 +50,31 @@ def main(args, seed=None):
|
||||
world.shuffle = args.shuffle.copy()
|
||||
world.logic = args.logic.copy()
|
||||
world.mode = args.mode.copy()
|
||||
world.swordless = args.swordless.copy()
|
||||
world.difficulty = args.difficulty.copy()
|
||||
world.item_functionality = args.item_functionality.copy()
|
||||
world.timer = args.timer.copy()
|
||||
world.goal = args.goal.copy()
|
||||
world.local_items = args.local_items.copy()
|
||||
|
||||
if hasattr(args, "algorithm"): # current GUI options
|
||||
world.algorithm = args.algorithm
|
||||
world.shuffleganon = args.shuffleganon
|
||||
world.custom = args.custom
|
||||
world.customitemarray = args.customitemarray
|
||||
|
||||
world.accessibility = args.accessibility.copy()
|
||||
world.retro = args.retro.copy()
|
||||
|
||||
world.hints = args.hints.copy()
|
||||
world.open_pyramid = args.open_pyramid.copy()
|
||||
world.boss_shuffle = args.shufflebosses.copy()
|
||||
world.enemy_shuffle = args.enemy_shuffle.copy()
|
||||
world.enemy_health = args.enemy_health.copy()
|
||||
world.enemy_damage = args.enemy_damage.copy()
|
||||
world.killable_thieves = args.killable_thieves.copy()
|
||||
world.bush_shuffle = args.bush_shuffle.copy()
|
||||
world.tile_shuffle = args.tile_shuffle.copy()
|
||||
world.beemizer = args.beemizer.copy()
|
||||
world.timer = args.timer.copy()
|
||||
world.countdown_start_time = args.countdown_start_time.copy()
|
||||
world.red_clock_time = args.red_clock_time.copy()
|
||||
world.blue_clock_time = args.blue_clock_time.copy()
|
||||
world.green_clock_time = args.green_clock_time.copy()
|
||||
world.shufflepots = args.shufflepots.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.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()
|
||||
world.progression_balancing = args.progression_balancing.copy()
|
||||
world.shuffle_prizes = args.shuffle_prizes.copy()
|
||||
world.sprite_pool = args.sprite_pool.copy()
|
||||
world.dark_room_logic = args.dark_room_logic.copy()
|
||||
@@ -93,12 +82,10 @@ def main(args, seed=None):
|
||||
world.plando_texts = args.plando_texts.copy()
|
||||
world.plando_connections = args.plando_connections.copy()
|
||||
world.er_seeds = getattr(args, "er_seeds", {})
|
||||
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
|
||||
world.required_medallions = args.required_medallions.copy()
|
||||
world.game = args.game.copy()
|
||||
world.set_options(args)
|
||||
world.player_name = args.name.copy()
|
||||
world.alttp_rom = args.rom
|
||||
world.enemizer = args.enemizercli
|
||||
world.sprite = args.sprite.copy()
|
||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
@@ -125,21 +112,22 @@ def main(args, seed=None):
|
||||
logger.info('')
|
||||
|
||||
for player in world.player_ids:
|
||||
for item_name in args.startinventory[player]:
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
for item_name, count in world.start_inventory[player].value.items():
|
||||
for _ in range(count):
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
|
||||
for player in world.player_ids:
|
||||
if player in world.get_game_players("A Link to the Past"):
|
||||
# enforce pre-defined local items.
|
||||
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
||||
world.local_items[player].add('Triforce Piece')
|
||||
world.local_items[player].value.add('Triforce Piece')
|
||||
|
||||
# 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']
|
||||
world.non_local_items[player].value -= item_name_groups['Pendants']
|
||||
world.non_local_items[player].value -= item_name_groups['Crystals']
|
||||
|
||||
# items can't be both local and non-local, prefer local
|
||||
world.non_local_items[player] -= world.local_items[player]
|
||||
world.non_local_items[player].value -= world.local_items[player].value
|
||||
|
||||
logger.info('Creating World.')
|
||||
AutoWorld.call_all(world, "create_regions")
|
||||
@@ -151,11 +139,14 @@ def main(args, seed=None):
|
||||
if world.players > 1:
|
||||
for player in world.player_ids:
|
||||
locality_rules(world, player)
|
||||
else:
|
||||
world.non_local_items[1].value = set()
|
||||
world.local_items[1].value = set()
|
||||
|
||||
AutoWorld.call_all(world, "set_rules")
|
||||
|
||||
for player in world.player_ids:
|
||||
exclusion_rules(world, player, args.excluded_locations[player])
|
||||
exclusion_rules(world, player, world.exclude_locations[player].value)
|
||||
|
||||
AutoWorld.call_all(world, "generate_basic")
|
||||
|
||||
@@ -246,8 +237,8 @@ def main(args, seed=None):
|
||||
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]]:
|
||||
for region in [world.get_region(take_any, player) for player in
|
||||
world.get_game_players("A Link to the Past") 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
|
||||
@@ -312,6 +303,8 @@ def main(args, seed=None):
|
||||
"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},
|
||||
"remote_start_inventory": {player for player in world.player_ids if
|
||||
world.worlds[player].remote_start_inventory},
|
||||
"locations": locations_data,
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": get_options()["server_options"],
|
||||
@@ -341,16 +334,16 @@ def main(args, seed=None):
|
||||
# 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:
|
||||
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
|
||||
if i % 10 == 0 or i == len(output_file_futures):
|
||||
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
|
||||
future.result()
|
||||
|
||||
if not args.skip_playthrough:
|
||||
if args.spoiler > 1:
|
||||
logger.info('Calculating playthrough.')
|
||||
create_playthrough(world)
|
||||
|
||||
if args.create_spoiler:
|
||||
if args.spoiler:
|
||||
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
|
||||
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
||||
@@ -393,7 +386,7 @@ def create_playthrough(world):
|
||||
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
|
||||
location.item.name, location.item.player, location.name, location.player) for location in
|
||||
sphere_candidates])
|
||||
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
|
||||
if any([world.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
|
||||
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
|
||||
f'Something went terribly wrong here.')
|
||||
else:
|
||||
|
||||
@@ -84,6 +84,7 @@ class Context:
|
||||
self.connect_names = {} # names of slots clients can connect to
|
||||
self.allow_forfeits = {}
|
||||
self.remote_items = set()
|
||||
self.remote_start_inventory = set()
|
||||
self.locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {}
|
||||
self.host = host
|
||||
self.port = port
|
||||
@@ -150,17 +151,33 @@ class Context:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint], msg: str) -> bool:
|
||||
sockets = []
|
||||
for endpoint in endpoints:
|
||||
if endpoint.socket and endpoint.socket.open:
|
||||
sockets.append(endpoint.socket)
|
||||
try:
|
||||
websockets.broadcast(sockets, msg)
|
||||
except RuntimeError:
|
||||
logging.exception("Exception during broadcast_send_encoded_msgs")
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing broadcast: {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))
|
||||
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth)
|
||||
asyncio.create_task(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
||||
|
||||
def broadcast_team(self, team, msgs):
|
||||
def broadcast_team(self, team: int, 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))
|
||||
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth and endpoint.team == team)
|
||||
asyncio.create_task(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
||||
|
||||
def broadcast(self, endpoints: typing.Iterable[Endpoint], msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
asyncio.create_task(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
||||
|
||||
async def disconnect(self, endpoint):
|
||||
if endpoint in self.endpoints:
|
||||
@@ -229,6 +246,7 @@ class Context:
|
||||
self.random.seed(self.seed_name)
|
||||
self.connect_names = decoded_obj['connect_names']
|
||||
self.remote_items = decoded_obj['remote_items']
|
||||
self.remote_start_inventory = decoded_obj.get('remote_start_inventory', decoded_obj['remote_items'])
|
||||
self.locations = decoded_obj['locations']
|
||||
self.slot_data = decoded_obj['slot_data']
|
||||
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
|
||||
@@ -237,7 +255,7 @@ class Context:
|
||||
# award remote-items start inventory:
|
||||
for team in range(len(decoded_obj['names'])):
|
||||
for slot, item_codes in decoded_obj["precollected_items"].items():
|
||||
if slot in self.remote_items:
|
||||
if slot in self.remote_start_inventory:
|
||||
self.received_items[team, slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
|
||||
for slot, hints in decoded_obj["precollected_hints"].items():
|
||||
self.hints[team, slot].update(hints)
|
||||
@@ -474,6 +492,10 @@ async def on_client_joined(ctx: Context, client: Client):
|
||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
||||
f"playing {ctx.games[client.slot]} has joined. "
|
||||
f"Client({version_str}), {client.tags}).")
|
||||
# TODO: remove with 0.2
|
||||
if client.version < Version(0, 1, 7):
|
||||
ctx.notify_client(client,
|
||||
"Warning: Your client's datapackage handling may be unsupported soon. (Version < 0.1.7)")
|
||||
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
@@ -1076,8 +1098,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
else:
|
||||
team, slot = ctx.connect_names[args['name']]
|
||||
game = ctx.games[slot]
|
||||
if args['game'] != game:
|
||||
errors.add('InvalidSlot')
|
||||
if "IgnoreGame" not in args["tags"] and args['game'] != game:
|
||||
errors.add('InvalidGame')
|
||||
# this can only ever be 0 or 1 elements
|
||||
clients = [c for c in ctx.endpoints if c.auth and c.slot == slot and c.team == team]
|
||||
if clients:
|
||||
|
||||
@@ -150,8 +150,8 @@ class JSONtoTextParser(metaclass=HandlerMeta):
|
||||
return "".join(self.handle_node(section) for section in input_object)
|
||||
|
||||
def handle_node(self, node: JSONMessagePart):
|
||||
type = node.get("type", None)
|
||||
handler = self.handlers.get(type, self.handlers["text"])
|
||||
node_type = node.get("type", None)
|
||||
handler = self.handlers.get(node_type, self.handlers["text"])
|
||||
return handler(node)
|
||||
|
||||
def _handle_color(self, node: JSONMessagePart):
|
||||
|
||||
111
Options.py
@@ -43,6 +43,9 @@ class Option(metaclass=AssembleOptions):
|
||||
# Handled in get_option_name()
|
||||
autodisplayname = False
|
||||
|
||||
# can be weighted between selections
|
||||
supports_weighting = True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.get_current_option_name()})"
|
||||
|
||||
@@ -81,6 +84,7 @@ class Toggle(Option):
|
||||
default = 0
|
||||
|
||||
def __init__(self, value: int):
|
||||
assert value == 0 or value == 1
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
@@ -119,6 +123,7 @@ class Toggle(Option):
|
||||
def get_option_name(cls, value):
|
||||
return ["No", "Yes"][int(value)]
|
||||
|
||||
|
||||
class DefaultOnToggle(Toggle):
|
||||
default = 1
|
||||
|
||||
@@ -150,19 +155,23 @@ class Choice(Option):
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, str):
|
||||
if isinstance(other, self.__class__):
|
||||
return other.value == self.value
|
||||
elif 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):
|
||||
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):
|
||||
if isinstance(other, self.__class__):
|
||||
return other.value != self.value
|
||||
elif isinstance(other, str):
|
||||
assert other in self.options
|
||||
return other != self.current_key
|
||||
elif isinstance(other, int):
|
||||
@@ -173,6 +182,7 @@ class Choice(Option):
|
||||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
|
||||
class Range(Option, int):
|
||||
range_start = 0
|
||||
range_end = 1
|
||||
@@ -230,6 +240,7 @@ class OptionNameSet(Option):
|
||||
|
||||
class OptionDict(Option):
|
||||
default = {}
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Dict[str, typing.Any]):
|
||||
self.value: typing.Dict[str, typing.Any] = value
|
||||
@@ -242,14 +253,17 @@ class OptionDict(Option):
|
||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||
|
||||
def get_option_name(self, value):
|
||||
return str(value)
|
||||
return ", ".join(f"{key}: {value}" for key, value in self.value.items())
|
||||
|
||||
|
||||
class OptionList(Option):
|
||||
class OptionList(Option, list):
|
||||
default = []
|
||||
supports_weighting = False
|
||||
value: list
|
||||
|
||||
def __init__(self, value: typing.List[str, typing.Any]):
|
||||
self.value = value
|
||||
super(OptionList, self).__init__()
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
@@ -262,23 +276,106 @@ class OptionList(Option):
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self, value):
|
||||
return str(value)
|
||||
return ", ".join(self.value)
|
||||
|
||||
|
||||
class OptionSet(Option, set):
|
||||
default = frozenset()
|
||||
supports_weighting = False
|
||||
value: set
|
||||
|
||||
def __init__(self, value: typing.Union[typing.Set[str, typing.Any], typing.List[str, typing.Any]]):
|
||||
self.value = set(value)
|
||||
super(OptionSet, self).__init__()
|
||||
|
||||
@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)
|
||||
elif type(data) == set:
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self, value):
|
||||
return ", ".join(self.value)
|
||||
|
||||
|
||||
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
||||
|
||||
|
||||
class Accessibility(Choice):
|
||||
"""Set rules for reachability of your items/locations.
|
||||
Locations: ensure everything can be reached and acquired.
|
||||
Items: ensure all logically relevant items can be acquired.
|
||||
Minimal: ensure what is needed to reach your goal can be acquired."""
|
||||
|
||||
option_locations = 0
|
||||
option_items = 1
|
||||
option_beatable = 2
|
||||
option_minimal = 2
|
||||
alias_none = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class ProgressionBalancing(DefaultOnToggle):
|
||||
"""A system that moves progression earlier, to try and prevent the player from getting stuck and bored early."""
|
||||
|
||||
|
||||
common_options = {
|
||||
"progression_balancing": ProgressionBalancing,
|
||||
"accessibility": Accessibility
|
||||
}
|
||||
|
||||
|
||||
class ItemSet(OptionSet):
|
||||
# implemented by Generate
|
||||
verify_item_name = True
|
||||
|
||||
|
||||
class LocalItems(ItemSet):
|
||||
"""Forces these items to be in their native world."""
|
||||
displayname = "Local Items"
|
||||
|
||||
|
||||
class NonLocalItems(ItemSet):
|
||||
"""Forces these items to be outside their native world."""
|
||||
displayname = "Not Local Items"
|
||||
|
||||
|
||||
class StartInventory(OptionDict):
|
||||
"""Start with these items."""
|
||||
verify_item_name = True
|
||||
displayname = "Start Inventory"
|
||||
|
||||
|
||||
class StartHints(ItemSet):
|
||||
"""Start with these item's locations prefilled into the !hint command."""
|
||||
displayname = "Start Hints"
|
||||
|
||||
|
||||
class ExcludeLocations(OptionSet):
|
||||
"""Prevent these locations from having an important item"""
|
||||
displayname = "Excluded Locations"
|
||||
verify_location_name = True
|
||||
|
||||
|
||||
per_game_common_options = {
|
||||
# placeholder until they're actually implemented
|
||||
"local_items": LocalItems,
|
||||
"non_local_items": NonLocalItems,
|
||||
"start_inventory": StartInventory,
|
||||
"start_hints": StartHints,
|
||||
"exclude_locations": OptionSet
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
from worlds.alttp.Options import Logic
|
||||
import argparse
|
||||
|
||||
map_shuffle = Toggle
|
||||
compass_shuffle = Toggle
|
||||
keyshuffle = Toggle
|
||||
|
||||
28
Utils.py
@@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.1.7"
|
||||
__version__ = "0.1.8"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
import builtins
|
||||
@@ -24,7 +24,7 @@ import pickle
|
||||
import functools
|
||||
import io
|
||||
import collections
|
||||
|
||||
import importlib
|
||||
from yaml import load, dump, safe_load
|
||||
|
||||
try:
|
||||
@@ -365,16 +365,28 @@ safe_builtins = {
|
||||
|
||||
|
||||
class RestrictedUnpickler(pickle.Unpickler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
||||
self.options_module = importlib.import_module("Options")
|
||||
self.net_utils_module = importlib.import_module("NetUtils")
|
||||
self.generic_properties_module = importlib.import_module("worlds.generic")
|
||||
|
||||
def find_class(self, module, name):
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
return getattr(builtins, name)
|
||||
# used by MultiServer -> savegame/multidata
|
||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}:
|
||||
import NetUtils
|
||||
return getattr(NetUtils, name)
|
||||
if module == "Options":
|
||||
import Options
|
||||
obj = getattr(Options, name)
|
||||
if issubclass(obj, Options.Option):
|
||||
return getattr(self.net_utils_module, name)
|
||||
# Options and Plando are unpickled by WebHost -> Generate
|
||||
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
|
||||
return getattr(self.generic_properties_module, name)
|
||||
if module.endswith("Options"):
|
||||
if module == "Options":
|
||||
mod = self.options_module
|
||||
else:
|
||||
mod = importlib.import_module(module)
|
||||
obj = getattr(mod, name)
|
||||
if issubclass(obj, self.options_module.Option):
|
||||
return obj
|
||||
# Forbid everything else.
|
||||
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
|
||||
|
||||
@@ -36,7 +36,11 @@ if __name__ == "__main__":
|
||||
multiprocessing.freeze_support()
|
||||
multiprocessing.set_start_method('spawn')
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
||||
update_sprites_lttp()
|
||||
try:
|
||||
update_sprites_lttp()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.warning("Could not update LttP sprites.")
|
||||
app = get_app()
|
||||
create_options_files()
|
||||
if app.config["SELFLAUNCH"]:
|
||||
|
||||
@@ -88,16 +88,10 @@ def player_settings(game):
|
||||
return render_template(f"player-settings.html", game=game)
|
||||
|
||||
|
||||
# Game sub-pages
|
||||
@app.route('/games/<string:game>/<string:page>')
|
||||
def game_pages(game, page):
|
||||
return render_template(f"/games/{game}/{page}.html")
|
||||
|
||||
|
||||
# Game landing pages
|
||||
@app.route('/games/<game>')
|
||||
def game_page(game):
|
||||
return render_template(f"/games/{game}/{game}.html")
|
||||
# Game Info Pages
|
||||
@app.route('/games/<string:game>/info/<string:lang>')
|
||||
def game_info(game, lang):
|
||||
return render_template('gameInfo.html', game=game, lang=lang)
|
||||
|
||||
|
||||
# List of supported games
|
||||
@@ -107,7 +101,7 @@ def games():
|
||||
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)
|
||||
return render_template("supportedGames.html", worlds=worlds)
|
||||
|
||||
|
||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||
|
||||
@@ -73,7 +73,8 @@ def roll_options(options: Dict[str, Union[dict, str]]) -> Tuple[Dict[str, Union[
|
||||
results[filename] = f"Failed to parse YAML data in {filename}: {e}"
|
||||
else:
|
||||
try:
|
||||
rolled_results[filename] = roll_settings(yaml_data, plando_options={"bosses"})
|
||||
rolled_results[filename] = roll_settings(yaml_data,
|
||||
plando_options={"bosses", "items", "connections", "texts"})
|
||||
except Exception as e:
|
||||
results[filename] = f"Failed to generate mystery in {filename}: {e}"
|
||||
else:
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
import tempfile
|
||||
import random
|
||||
import json
|
||||
import zipfile
|
||||
from collections import Counter
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
@@ -15,6 +16,7 @@ import pickle
|
||||
from .models import *
|
||||
from WebHostLib import app
|
||||
from .check import get_yaml_data, roll_options
|
||||
from .upload import upload_zip_to_db
|
||||
|
||||
|
||||
@app.route('/generate', methods=['GET', 'POST'])
|
||||
@@ -69,14 +71,13 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||
if race:
|
||||
random.seed() # reset to time-based random source
|
||||
|
||||
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
|
||||
seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
|
||||
|
||||
erargs = parse_arguments(['--multi', str(playercount)])
|
||||
erargs.seed = seed
|
||||
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwrittin in mystery
|
||||
erargs.create_spoiler = not race
|
||||
erargs.spoiler = 0 if race else 2
|
||||
erargs.race = race
|
||||
erargs.skip_playthrough = race
|
||||
erargs.outputname = seedname
|
||||
erargs.outputpath = target.name
|
||||
erargs.teams = 1
|
||||
@@ -85,7 +86,10 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||
for k, v in settings.items():
|
||||
if v is not None:
|
||||
getattr(erargs, k)[player] = v
|
||||
if hasattr(erargs, k):
|
||||
getattr(erargs, k)[player] = v
|
||||
else:
|
||||
setattr(erargs, k, {player: v})
|
||||
|
||||
if not erargs.name[player]:
|
||||
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
|
||||
@@ -93,7 +97,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||
|
||||
ERmain(erargs, seed)
|
||||
|
||||
return upload_to_db(target.name, owner, sid, race)
|
||||
return upload_to_db(target.name, sid, owner, race)
|
||||
except BaseException as e:
|
||||
if sid:
|
||||
with db_session:
|
||||
@@ -123,37 +127,19 @@ def wait_seed(seed: UUID):
|
||||
return render_template("waitSeed.html", seed_id=seed_id)
|
||||
|
||||
|
||||
def upload_to_db(folder, owner, sid, race:bool):
|
||||
slots = set()
|
||||
spoiler = ""
|
||||
|
||||
multidata = None
|
||||
def upload_to_db(folder, sid, owner, race):
|
||||
for file in os.listdir(folder):
|
||||
file = os.path.join(folder, file)
|
||||
if file.endswith(".apbp"):
|
||||
player_text = file.split("_P", 1)[1]
|
||||
player_name = player_text.split("_", 1)[1].split(".", 1)[0]
|
||||
player_id = int(player_text.split(".", 1)[0].split("_", 1)[0])
|
||||
slots.add(Slot(data=open(file, "rb").read(),
|
||||
player_id=player_id, player_name = player_name, game = "A Link to the Past"))
|
||||
elif file.endswith(".txt"):
|
||||
spoiler = open(file, "rt", encoding="utf-8-sig").read()
|
||||
elif file.endswith(".archipelago"):
|
||||
multidata = open(file, "rb").read()
|
||||
if multidata:
|
||||
with db_session:
|
||||
if sid:
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner,
|
||||
id=sid, meta=json.dumps({"race": race, "tags": ["generated"]}))
|
||||
else:
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner,
|
||||
meta=json.dumps({"race": race, "tags": ["generated"]}))
|
||||
for patch in slots:
|
||||
patch.seed = seed
|
||||
if sid:
|
||||
gen = Generation.get(id=sid)
|
||||
if gen is not None:
|
||||
gen.delete()
|
||||
return seed.id
|
||||
else:
|
||||
raise Exception("Multidata required (.archipelago), but not found.")
|
||||
if file.endswith(".zip"):
|
||||
with db_session:
|
||||
with zipfile.ZipFile(file) as zfile:
|
||||
res = upload_zip_to_db(zfile, owner, {"race": race}, sid)
|
||||
if type(res) == "str":
|
||||
raise Exception(res)
|
||||
elif res:
|
||||
seed = res
|
||||
gen = Generation.get(id=seed.id)
|
||||
if gen is not None:
|
||||
gen.delete()
|
||||
return seed.id
|
||||
raise Exception("Generation zipfile not found.")
|
||||
|
||||
@@ -5,6 +5,7 @@ import yaml
|
||||
import json
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
import Options
|
||||
|
||||
target_folder = os.path.join("WebHostLib", "static", "generated")
|
||||
|
||||
@@ -18,10 +19,17 @@ def create():
|
||||
option.range_end: "maximum value"
|
||||
}
|
||||
return data, notes
|
||||
|
||||
def default_converter(default_value):
|
||||
if isinstance(default_value, (set, frozenset)):
|
||||
return list(default_value)
|
||||
return default_value
|
||||
|
||||
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,
|
||||
dictify_range=dictify_range
|
||||
options={**world.options, **Options.per_game_common_options},
|
||||
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||
dictify_range=dictify_range, default_converter=default_converter,
|
||||
)
|
||||
|
||||
with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f:
|
||||
@@ -31,6 +39,7 @@ def create():
|
||||
player_settings = {
|
||||
"baseOptions": {
|
||||
"description": "Generated by https://archipelago.gg/",
|
||||
"game": game_name,
|
||||
"name": "Player",
|
||||
},
|
||||
}
|
||||
@@ -46,7 +55,7 @@ def create():
|
||||
"options": []
|
||||
}
|
||||
|
||||
for sub_option_name, sub_option_id in option.options.items():
|
||||
for sub_option_id, sub_option_name in option.name_lookup.items():
|
||||
this_option["options"].append({
|
||||
"name": option.get_option_name(sub_option_id),
|
||||
"value": sub_option_name,
|
||||
|
||||
@@ -19,6 +19,9 @@ window.addEventListener('load', () => {
|
||||
ajax.send();
|
||||
}).then((results) => {
|
||||
// Populate page with HTML generated from markdown
|
||||
showdown.setOption('tables', true);
|
||||
showdown.setOption('strikethrough', true);
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
|
||||
@@ -1,10 +1,52 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
## What is a randomizer?
|
||||
Who's on first.
|
||||
A randomizer is a modification of a video game which reorganizes the items required to progress through the game.
|
||||
A normal play-through of a game might require you to use item A to unlock item B, then C, and so forth. In a
|
||||
randomized game, you might first find item C, then A, then B.
|
||||
|
||||
This transforms games from a linear experience into a puzzle, presenting players with a new challenge each time they
|
||||
play a randomized game. Putting items in non-standard locations can require the player to think about the game world
|
||||
and the items they encounter in new and interesting ways.
|
||||
|
||||
## What happens if an item is placed somewhere it is impossible to get?
|
||||
The randomizer has many strict sets of rules it must follow when generating a game. One of the functions of these
|
||||
rules is to ensure items necessary to complete the game will be accessible to the player. Many games also have a
|
||||
subset of rules allowing certain items to be placed in normally unreachable locations, provided the player has
|
||||
indicated they are comfortable exploiting certain glitches in the game.
|
||||
|
||||
## What is a multi-world?
|
||||
What's on second.
|
||||
While a randomizer shuffles a game, a multi-world randomizer shuffles that game for multiple players. For example,
|
||||
in a two player multi-world, players A and B each get their own randomized version of a game, called seeds. In each
|
||||
player's game, they may find items which belong to the other player. If player A finds an item which belongs to
|
||||
player B, the item will be sent to player B's world over the internet.
|
||||
|
||||
This creates a cooperative experience during multi-world games, requiring players to rely upon each other to complete
|
||||
their game. Currently, a maximum of 255 players can participate in a single multi-world.
|
||||
|
||||
## What happens if a person has to leave early?
|
||||
If a player must leave early, they can use Archipelago's forfeit system. When a player forfeits their game, all
|
||||
the items in that game which belong to other players are sent out automatically, so other players can continue to
|
||||
play.
|
||||
|
||||
## What does multi-game mean?
|
||||
I don't know's on third.
|
||||
While a multi-world game traditionally requires all players to be playing the same game, a multi-game multi-world
|
||||
allows players to randomize any of a number of supported games, and send items between them. This allows players of
|
||||
different games to interact with one another in a single multiplayer environment.
|
||||
|
||||
## Can I generate a single-player game with Archipelago?
|
||||
Yes. All our supported games can be generated as single-player experiences, and so long as you download the software,
|
||||
the website is not required to generate them.
|
||||
|
||||
## How do I get started?
|
||||
If you are ready to start randomizing games, or want to start playing your favorite randomizer with others,
|
||||
please join our [Discord server](https://discord.gg/8Z65BR2). There are always people ready to answer any questions
|
||||
you might have.
|
||||
|
||||
## I want to add a game to the Archipelago randomizer. How do I do that?
|
||||
The best way to get started is to take a look at our [code on GitHub](https://github.com/ArchipelagoMW/Archipelago).
|
||||
There, you will find examples of games in the [worlds](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds)
|
||||
folder, as well as some [documentation](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs) on our
|
||||
network interfaces.
|
||||
|
||||
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.
|
||||
|
||||
53
WebHostLib/static/assets/gameInfo.js
Normal file
@@ -0,0 +1,53 @@
|
||||
window.addEventListener('load', () => {
|
||||
const gameInfo = document.getElementById('game-info');
|
||||
new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status === 404) {
|
||||
reject("Sorry, this game's info page is not available in that language yet.");
|
||||
return;
|
||||
}
|
||||
if (ajax.status !== 200) {
|
||||
reject("Something went wrong while loading the info page.");
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/assets/gameInfo/` +
|
||||
`${gameInfo.getAttribute('data-lang')}_${gameInfo.getAttribute('data-game')}.md`, true);
|
||||
ajax.send();
|
||||
}).then((results) => {
|
||||
// Populate page with HTML generated from markdown
|
||||
showdown.setOption('tables', true);
|
||||
showdown.setOption('strikethrough', true);
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
gameInfo.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);
|
||||
gameInfo.innerHTML =
|
||||
`<h2>This page is out of logic!</h2>
|
||||
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
|
||||
});
|
||||
});
|
||||
26
WebHostLib/static/assets/gameInfo/en_A Link to the Past.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# A Link to the Past
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game
|
||||
is always able to be completed, but because of the item shuffle the player may need to access certain areas before
|
||||
they would in the vanilla game.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
All main inventory items, collectables, and ammunition can be shuffled, and all locations in the game which could
|
||||
contain any of those items may have their contents changed.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
|
||||
limit certain items to your own world.
|
||||
|
||||
## What does another world's item look like in LttP?
|
||||
Items belonging to other worlds are represented by a Power Star from Super Mario World.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
When the player receives an item, Link will hold the item above his head and display it to the world. It's good for
|
||||
business!
|
||||
|
||||
29
WebHostLib/static/assets/gameInfo/en_Factorio.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Factorio
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
In Factorio, the research tree is shuffled, causing certain technologies to be obtained in a non-standard order.
|
||||
Recipe costs, technology requirements, and science pack requirements may also be shuffled at the player's discretion.
|
||||
|
||||
## What Factorio items can appear in other players' worlds?
|
||||
Factorio's technologies are removed from its tech tree and placed into other players' worlds. When those technologies
|
||||
are found, they are sent back to Factorio along with, optionally, free samples of those technologies.
|
||||
|
||||
## What is a free sample?
|
||||
A free sample is a single or stack of items in Factorio, granted by a technology received from another world. For
|
||||
example, receiving the technology
|
||||
`Portable Solar Panel` may also grant the player a stack of portable solar panels,
|
||||
and place them directly into the player's inventory.
|
||||
|
||||
## What does another world's item look like in Factorio?
|
||||
In Factorio, items which need to be sent to other worlds appear in the tech tree as new research items. They are
|
||||
represented by the Archipelago icon, and must be researched as if it were a normal technology. Upon successful
|
||||
completion of research, the item will be sent to its home world.
|
||||
|
||||
## When the engineer receives an item, what happens?
|
||||
When the player receives a technology, it is instantly learned and able to be crafted. A message will appear in the
|
||||
chat log to notify the player, and if free samples are enabled the player may also receive some items directly to
|
||||
their inventory.
|
||||
26
WebHostLib/static/assets/gameInfo/en_Ocarina of Time.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Ocarina of Time
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game
|
||||
is always able to be completed, but because of the item shuffle the player may need to access certain areas before
|
||||
they would in the vanilla game.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
All main inventory items, collectables, and ammunition can be shuffled, and all locations in the game which could
|
||||
contain any of those items may have their contents changed. Gold Skultulla locations may also be included as necessary
|
||||
checks at the user's discretion.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
|
||||
limit certain items to your own world.
|
||||
|
||||
## What does another world's item look like in OoT?
|
||||
Items belonging to other worlds are represented by an Ocarina of Time.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
When the player receives an item, Link will hold the item above his head and display it to the world. It's good for
|
||||
business!
|
||||
27
WebHostLib/static/assets/gameInfo/en_Subnautica.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Subnautica
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
The most noticeable change is the complete removal of freestanding technologies. The technology blueprints normally
|
||||
awarded from scanning those items have been shuffled into location checks throughout the AP item pool.
|
||||
|
||||
## What is the goal of Subnautica when randomized?
|
||||
The goal remains unchanged. Cure the plague, build the Neptune Escape Rocket, and escape into space.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
Most of the technologies the player will need throughout the game will be shuffled. Location checks in Subnautica are
|
||||
data pads and technology lockers.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
Most technologies may be shuffled into another player's world.
|
||||
|
||||
## What does another world's item look like in Subnautica?
|
||||
Location checks in Subnautica are data pads and technology lockers. Opening one of these will send an item to
|
||||
another player's world.
|
||||
|
||||
## When the player receives a technology, what happens?
|
||||
When the player receives a technology, the chat log displays a notification the technology has been received.
|
||||
|
||||
2
WebHostLib/static/assets/md5.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// Copyright © 2011 Sebastian Tschan, https://blueimp.net
|
||||
!function(n){"use strict";function d(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d((c=d(d(t,n),d(e,u)))<<(f=o)|c>>>32-f,r);var c,f}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u;n[t>>5]|=128<<t%32,n[14+(t+64>>>9<<4)]=t;for(var c=1732584193,f=-271733879,i=-1732584194,a=271733878,h=0;h<n.length;h+=16)c=l(r=c,e=f,o=i,u=a,n[h],7,-680876936),a=l(a,c,f,i,n[h+1],12,-389564586),i=l(i,a,c,f,n[h+2],17,606105819),f=l(f,i,a,c,n[h+3],22,-1044525330),c=l(c,f,i,a,n[h+4],7,-176418897),a=l(a,c,f,i,n[h+5],12,1200080426),i=l(i,a,c,f,n[h+6],17,-1473231341),f=l(f,i,a,c,n[h+7],22,-45705983),c=l(c,f,i,a,n[h+8],7,1770035416),a=l(a,c,f,i,n[h+9],12,-1958414417),i=l(i,a,c,f,n[h+10],17,-42063),f=l(f,i,a,c,n[h+11],22,-1990404162),c=l(c,f,i,a,n[h+12],7,1804603682),a=l(a,c,f,i,n[h+13],12,-40341101),i=l(i,a,c,f,n[h+14],17,-1502002290),c=v(c,f=l(f,i,a,c,n[h+15],22,1236535329),i,a,n[h+1],5,-165796510),a=v(a,c,f,i,n[h+6],9,-1069501632),i=v(i,a,c,f,n[h+11],14,643717713),f=v(f,i,a,c,n[h],20,-373897302),c=v(c,f,i,a,n[h+5],5,-701558691),a=v(a,c,f,i,n[h+10],9,38016083),i=v(i,a,c,f,n[h+15],14,-660478335),f=v(f,i,a,c,n[h+4],20,-405537848),c=v(c,f,i,a,n[h+9],5,568446438),a=v(a,c,f,i,n[h+14],9,-1019803690),i=v(i,a,c,f,n[h+3],14,-187363961),f=v(f,i,a,c,n[h+8],20,1163531501),c=v(c,f,i,a,n[h+13],5,-1444681467),a=v(a,c,f,i,n[h+2],9,-51403784),i=v(i,a,c,f,n[h+7],14,1735328473),c=g(c,f=v(f,i,a,c,n[h+12],20,-1926607734),i,a,n[h+5],4,-378558),a=g(a,c,f,i,n[h+8],11,-2022574463),i=g(i,a,c,f,n[h+11],16,1839030562),f=g(f,i,a,c,n[h+14],23,-35309556),c=g(c,f,i,a,n[h+1],4,-1530992060),a=g(a,c,f,i,n[h+4],11,1272893353),i=g(i,a,c,f,n[h+7],16,-155497632),f=g(f,i,a,c,n[h+10],23,-1094730640),c=g(c,f,i,a,n[h+13],4,681279174),a=g(a,c,f,i,n[h],11,-358537222),i=g(i,a,c,f,n[h+3],16,-722521979),f=g(f,i,a,c,n[h+6],23,76029189),c=g(c,f,i,a,n[h+9],4,-640364487),a=g(a,c,f,i,n[h+12],11,-421815835),i=g(i,a,c,f,n[h+15],16,530742520),c=m(c,f=g(f,i,a,c,n[h+2],23,-995338651),i,a,n[h],6,-198630844),a=m(a,c,f,i,n[h+7],10,1126891415),i=m(i,a,c,f,n[h+14],15,-1416354905),f=m(f,i,a,c,n[h+5],21,-57434055),c=m(c,f,i,a,n[h+12],6,1700485571),a=m(a,c,f,i,n[h+3],10,-1894986606),i=m(i,a,c,f,n[h+10],15,-1051523),f=m(f,i,a,c,n[h+1],21,-2054922799),c=m(c,f,i,a,n[h+8],6,1873313359),a=m(a,c,f,i,n[h+15],10,-30611744),i=m(i,a,c,f,n[h+6],15,-1560198380),f=m(f,i,a,c,n[h+13],21,1309151649),c=m(c,f,i,a,n[h+4],6,-145523070),a=m(a,c,f,i,n[h+11],10,-1120210379),i=m(i,a,c,f,n[h+2],15,718787259),f=m(f,i,a,c,n[h+9],21,-343485551),c=d(c,r),f=d(f,e),i=d(i,o),a=d(a,u);return[c,f,i,a]}function a(n){for(var t="",r=32*n.length,e=0;e<r;e+=8)t+=String.fromCharCode(n[e>>5]>>>e%32&255);return t}function h(n){var t=[];for(t[(n.length>>2)-1]=void 0,e=0;e<t.length;e+=1)t[e]=0;for(var r=8*n.length,e=0;e<r;e+=8)t[e>>5]|=(255&n.charCodeAt(e/8))<<e%32;return t}function e(n){for(var t,r="0123456789abcdef",e="",o=0;o<n.length;o+=1)t=n.charCodeAt(o),e+=r.charAt(t>>>4&15)+r.charAt(15&t);return e}function r(n){return unescape(encodeURIComponent(n))}function o(n){return a(i(h(t=r(n)),8*t.length));var t}function u(n,t){return function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16<o.length&&(o=i(o,8*n.length)),r=0;r<16;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(h(t)),512+8*t.length),a(i(c.concat(e),640))}(r(n),r(t))}function t(n,t,r){return t?r?u(t,n):e(u(t,n)):r?o(n):e(o(n))}"function"==typeof define&&define.amd?define(function(){return t}):"object"==typeof module&&module.exports?module.exports=t:n.md5=t}(this);
|
||||
52
WebHostLib/static/assets/ootTracker.js
Normal file
@@ -0,0 +1,52 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Reload tracker every 15 seconds
|
||||
const url = window.location;
|
||||
setInterval(() => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
|
||||
// Create a fake DOM using the returned HTML
|
||||
const domParser = new DOMParser();
|
||||
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||
|
||||
// Update item tracker
|
||||
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||
// Update only counters, small keys, and boss keys in the location-table
|
||||
const types = ['counter', 'smallkeys', 'bosskeys'];
|
||||
for (let j = 0; j < types.length; j++) {
|
||||
let counters = document.getElementsByClassName(types[j]);
|
||||
const fakeCounters = fakeDOM.getElementsByClassName(types[j]);
|
||||
for (let i = 0; i < counters.length; i++) {
|
||||
counters[i].innerHTML = fakeCounters[i].innerHTML;
|
||||
}
|
||||
}
|
||||
};
|
||||
ajax.open('GET', url);
|
||||
ajax.send();
|
||||
}, 15000)
|
||||
|
||||
// Collapsible advancement sections
|
||||
const categories = document.getElementsByClassName("location-category");
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
let hide_id = categories[i].id.split('-')[0];
|
||||
if (hide_id == 'Total') {
|
||||
continue;
|
||||
}
|
||||
categories[i].addEventListener('click', function() {
|
||||
// Toggle the advancement list
|
||||
document.getElementById(hide_id).classList.toggle("hide");
|
||||
// Change text of the header
|
||||
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||
const orig_text = tab_header.innerHTML;
|
||||
let new_text;
|
||||
if (orig_text.includes("▼")) {
|
||||
new_text = orig_text.replace("▼", "▲");
|
||||
}
|
||||
else {
|
||||
new_text = orig_text.replace("▲", "▼");
|
||||
}
|
||||
tab_header.innerHTML = new_text;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -7,6 +7,22 @@ window.addEventListener('load', () => {
|
||||
document.getElementById('game-name').innerHTML = gameName;
|
||||
|
||||
Promise.all([fetchSettingData()]).then((results) => {
|
||||
let settingHash = localStorage.getItem(`${gameName}-hash`);
|
||||
if (!settingHash) {
|
||||
// If no hash data has been set before, set it now
|
||||
localStorage.setItem(`${gameName}-hash`, md5(results[0]));
|
||||
localStorage.removeItem(gameName);
|
||||
settingHash = md5(results[0]);
|
||||
}
|
||||
|
||||
if (settingHash !== md5(results[0])) {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = "Your settings are out of date! Click here to update them! Be aware this will reset " +
|
||||
"them all to default.";
|
||||
userMessage.style.display = "block";
|
||||
userMessage.addEventListener('click', resetSettings);
|
||||
}
|
||||
|
||||
// Page setup
|
||||
createDefaultSettings(results[0]);
|
||||
buildUI(results[0]);
|
||||
@@ -14,7 +30,7 @@ window.addEventListener('load', () => {
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true))
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||
|
||||
// Name input field
|
||||
@@ -28,6 +44,12 @@ window.addEventListener('load', () => {
|
||||
})
|
||||
});
|
||||
|
||||
const resetSettings = () => {
|
||||
localStorage.removeItem(gameName);
|
||||
localStorage.removeItem(`${gameName}-hash`)
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const fetchSettingData = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
|
||||
@@ -20,6 +20,9 @@ window.addEventListener('load', () => {
|
||||
ajax.send();
|
||||
}).then((results) => {
|
||||
// Populate page with HTML generated from markdown
|
||||
showdown.setOption('tables', true);
|
||||
showdown.setOption('strikethrough', true);
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
|
||||
@@ -1,52 +1,12 @@
|
||||
# Guia instalación de Minecraft Randomizer
|
||||
|
||||
#Instalacion automatica para el huesped de partida
|
||||
- descarga e instala [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and activa el modulo `Minecraft Client`
|
||||
|
||||
## Software Requerido
|
||||
|
||||
### Servidor
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
|
||||
### Jugadores
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
|
||||
## Procedimiento de instalación
|
||||
|
||||
### Instalación de servidor dedicado
|
||||
Solo una persona ha de realizar este proceso y hospedar un servidor dedicado para que los demas jueguen conectandose a él.
|
||||
1. Descarga el instalador de **Minecraft Forge** 1.16.15 desde el enlace proporcionado, siempre asegurandose de bajar la version mas reciente.
|
||||
|
||||
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elije **install server**.
|
||||
- En esta pagina elegiras ademas donde instalar el servidor, importante recordar esta localización en el siguiente paso.
|
||||
|
||||
3. Navega al directorio donde hayas instalado el servidor y abre `forge-1.16.5-xx.x.x.jar`
|
||||
- La primera vez que lances el servidor se cerrara (o no aparecerá nada en absoluto), debería haber un fichero nuevo en el directorio llamado `eula.txt`, el cual que contiene un enlace al EULA de minecraft, cambia la linea a `eula=true` para aceptar el EULA y poder utilizar el software de servidor.
|
||||
- Esto creara la estructura de directorios apropiada para el siguiente paso
|
||||
|
||||
4. Coloca el fichero `aprandomizer-x.x.x.jar` del segundo enlace en el directorio `mods`
|
||||
- Cuando se ejecute el servidor de nuevo, generara el directorio `APData` que se necesitara para jugar
|
||||
|
||||
### Instalación basica para jugadores
|
||||
- Compra e instala Minecraft a traves del tercer enlace.
|
||||
**Y listo!**.
|
||||
Los jugadores solo necesitan una version no modificada de Minecraft para jugar!
|
||||
|
||||
### Instalación avanzada para jugadores
|
||||
***Esto no es requerido para jugar a minecraft randomizado.***
|
||||
Sin embargo lo recomendamos porque hace la experiencia mas llevadera.
|
||||
|
||||
#### Recomended Mods
|
||||
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
|
||||
|
||||
|
||||
1. Instala y ejecuta Minecraft al menos una vez.
|
||||
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elige **install client**.
|
||||
- Ejecuta Minecraft forge al menos una vez para generar los directorios necesarios para el siguiente paso.
|
||||
3. Navega a la carpeta de instalación de Minecraft y colocal los mods que quieras en el directorio `mods`
|
||||
- Los directorios por defecto de instalación son:
|
||||
- Windows `%APPDATA%\.minecraft\mods`
|
||||
- macOS `~/Library/Application Support/minecraft/mods`
|
||||
- Linux `~/.minecraft/mods`
|
||||
|
||||
## Configura tu fichero YAML
|
||||
|
||||
### Que es un fichero YAML y potque necesito uno?
|
||||
@@ -58,42 +18,71 @@ pueden tener diferentes opciones
|
||||
### Where do I get a YAML file?
|
||||
Un fichero basico yaml para minecraft tendra este aspecto.
|
||||
```yaml
|
||||
# Usado para describir tu yaml. Util si tienes multiples ficheros
|
||||
description: Template Name
|
||||
# Tu nombre en el juego. Los espacios son reemplazados por guiones bajos, limitado a 16 caracteres
|
||||
name: YourName
|
||||
description: Basic Minecraft Yaml
|
||||
# Tu nombre en el juego. Espacios seran sustituidos por guinoes bajos y
|
||||
# hay un limite de 16 caracteres
|
||||
name: TuNombre
|
||||
game: Minecraft
|
||||
accessibility: locations
|
||||
# Recomendado no activar esto ya que el pool de objetos de Minecraft es bastante escueto, ademas hay muchas maneras alternativas de obtener los objetivos de Minecraft.
|
||||
progression_balancing: off
|
||||
# Cuantos avances se necesitan para hacer aparecer el Ender Dragon y acabar el juego. few = 30, normal = 50 , many = 70
|
||||
advancement_goal:
|
||||
few: 0
|
||||
normal: 1
|
||||
many: 0
|
||||
# Modifica el nivel de objetos lógicamente requeridos para explorar areas peligrosas y pelear contra jefes.
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
# Avances que sean tediosos o basados en suerte tendran simplemente experiencia o cosas no necesarias
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Los avances extremadamente difíciles no seran requeridos; esto afecta a How Did We Get Here? y Adventuring Time.
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Los avances posteriores a Ender Dragon no tendrán objetos necesarios para que otros jugadores en el caso de un MW acaben su partida.
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Actualmente desactivado; permite la mezcla de pueblos, puestos, fortalezas, bastiones y cuidades.
|
||||
shuffle_structures:
|
||||
on: 0
|
||||
off: 1
|
||||
```
|
||||
|
||||
# Opciones compartidas por todos los juegos:
|
||||
accessibility: locations
|
||||
progression_balancing: on
|
||||
# Opciones Especficicas para Minecraft
|
||||
|
||||
Minecraft:
|
||||
# Numero de logros requeridos (87 max) para que aparezca el Ender Dragon y completar el juego.
|
||||
advancement_goal: 50
|
||||
|
||||
# Numero de trozos de huevo de dragon a obtener (30 max) antes de que el Ender Dragon aparezca.
|
||||
egg_shards_required: 10
|
||||
|
||||
# Numero de huevos disponibles en la partida (30 max).
|
||||
egg_shards_available: 15
|
||||
|
||||
# Modifica el nivel de objetos logicamente requeridos para
|
||||
# explorar areas peligrosas y luchar contra jefes.
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
|
||||
# Si off, los logros que dependan de suerte o sean tediosos tendran objetos de apoyo, no necesarios para completar el juego.
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Si off, los logros muy dificiles tendran objetos de apoyo, no necesarios para completar el juego.
|
||||
# Solo afecta a How Did We Get Here? and Adventuring Time.
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Algunos logros requieren derrotar al Ender Dragon;
|
||||
# Si esto se queda en off, dichos logros no tendran objetos necesarios.
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Permite el mezclado de villas, puesto, fortalezas, bastiones y ciudades de END.
|
||||
shuffle_structures:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Añade brujulas de estructura al juego,
|
||||
# apuntaran a la estructura correspondiente mas cercana.
|
||||
structure_compasses:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Reemplaza un porcentaje de objetos innecesarios por trampas abeja
|
||||
# las cuales crearan multiples abejas agresivas alrededor de los jugadores cuando se reciba.
|
||||
bee_traps:
|
||||
0: 1
|
||||
25: 0
|
||||
50: 0
|
||||
75: 0
|
||||
100: 0
|
||||
```
|
||||
|
||||
## Unirse a un juego MultiWorld
|
||||
|
||||
@@ -104,18 +93,39 @@ Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAM
|
||||
Una vez la generación acabe, el anfitrión te dará un enlace a tu fichero de datos o un zip con los ficheros de todos.
|
||||
Tu fichero de datos tiene una extensión `.apmc`.
|
||||
|
||||
Pon tu fichero de datos en el directorio `APData` de tu forge server. Asegurate de eliminar los que hubiera anteriormente
|
||||
|
||||
Haz doble click en tu fichero `.apmc` para que se arranque el cliente de minecraft y el servidor forge se ejecute.
|
||||
|
||||
### Conectar al multiserver
|
||||
Despues de poner tu fichero en el directorio `APData`, arranca el Forge server y asegurate que tienes el estado OP
|
||||
tecleando `/op TuUsuarioMinecraft` en la consola del servidor y entonces conectate con tu cliente Minecraft.
|
||||
|
||||
Una vez en juego introduce `/connect <AP-Address> (<Password>)` donde `<AP-Address>` es la dirección del servidor
|
||||
Archipelago. `(<Password>)`
|
||||
Una vez en juego introduce `/connect <AP-Address> (Port) (<Password>)` donde `<AP-Address>` es la dirección del servidor. `(Port)` solo es requerido si el servidor Archipelago no esta usando el puerto por defecto 38281.
|
||||
`(<Password>)`
|
||||
solo se necesita si el servidor Archipleago tiene un password activo.
|
||||
|
||||
|
||||
### Jugar al juego
|
||||
Cuando la consola te diga que te has unido a la sala, estas lista/o para empezar a jugar. Felicidades
|
||||
por unirte exitosamente a un juego multiworld! Llegados a este punto cualquier jugador adicional puede conectarse a tu servidor forge.
|
||||
|
||||
## Procedimiento de instalación manual
|
||||
Solo es requerido si quieres usar una instalacion de forge por ti mismo, recomendamos usar el instalador de Archipelago
|
||||
###Software Requerido
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
**NO INSTALES ESTO EN TU CLIENTE MINECRAFT**
|
||||
|
||||
|
||||
### Instalación de servidor dedicado
|
||||
Solo una persona ha de realizar este proceso y hospedar un servidor dedicado para que los demas jueguen conectandose a él.
|
||||
1. Descarga el instalador de **Minecraft Forge** 1.16.5 desde el enlace proporcionado, siempre asegurandose de bajar la version mas reciente.
|
||||
|
||||
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elije **install server**.
|
||||
- En esta pagina elegiras ademas donde instalar el servidor, importante recordar esta localización en el siguiente paso.
|
||||
|
||||
3. Navega al directorio donde hayas instalado el servidor y abre `forge-1.16.5-xx.x.x.jar`
|
||||
- La primera vez que lances el servidor se cerrara (o no aparecerá nada en absoluto), debería haber un fichero nuevo en el directorio llamado `eula.txt`, el cual que contiene un enlace al EULA de minecraft, cambia la linea a `eula=true` para aceptar el EULA y poder utilizar el software de servidor.
|
||||
- Esto creara la estructura de directorios apropiada para el siguiente paso
|
||||
|
||||
4. Coloca el fichero `aprandomizer-x.x.x.jar` del segundo enlace en el directorio `mods`
|
||||
- Cuando se ejecute el servidor de nuevo, generara el directorio `APData` que se necesitara para jugar
|
||||
|
||||
@@ -46,6 +46,28 @@ Risk of Rain 2:
|
||||
start_with_revive: true
|
||||
item_pickup_step: 1
|
||||
enable_lunar: true
|
||||
item_weights:
|
||||
default: 50
|
||||
new: 0
|
||||
uncommon: 0
|
||||
legendary: 0
|
||||
lunartic: 0
|
||||
chaos: 0
|
||||
no_scraps: 0
|
||||
even: 0
|
||||
scraps_only: 0
|
||||
item_pool_presets: true
|
||||
# custom item weights
|
||||
green_scrap: 16
|
||||
red_scrap: 4
|
||||
yellow_scrap: 1
|
||||
white_scrap: 32
|
||||
common_item: 64
|
||||
uncommon_item: 32
|
||||
legendary_item: 8
|
||||
boss_item: 4
|
||||
lunar_item: 16
|
||||
equipment: 32
|
||||
```
|
||||
|
||||
| Name | Description | Allowed values |
|
||||
@@ -55,6 +77,10 @@ Risk of Rain 2:
|
||||
| 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 |
|
||||
| item_weights | Each option here is a preset item weight that can be used to customize your generate item pool with certain settings. | default, new, uncommon, legendary, lunartic, chaos, no_scraps, even, scraps_only |
|
||||
| item_pool_presets | A simple toggle to determine whether the item_weight presets are used or the custom item pool as defined below | true/false |
|
||||
| custom item weights | Each defined item here is a single item in the pool that will have a weight against the other items when the item pool gets generated. These values can be modified to adjust how frequently certain items appear | 0-100|
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -66,4 +92,6 @@ They will have 4 of the items which other players can grant them replaced with `
|
||||
|
||||
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)
|
||||
The player will have lunar items shuffled into the item pool on their behalf. (enable_lunar = true)
|
||||
|
||||
The player will have the default preset generated item pool with the custom item weights being ignored. (item_weights: default and item_pool_presets: true)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Benötigte Software
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
|
||||
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien
|
||||
- Ein Emulator, der lua-scripts abspielen kann
|
||||
- [SNI](https://github.com/alttpo/sni/releases) (Integriert in Archipelago)
|
||||
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien fähig zu einer Internetverbindung
|
||||
- Ein Emulator, der mit SNI verbinden kann
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
[BizHawk](http://tasvideos.org/BizHawk.html))
|
||||
- Ein SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), oder andere kompatible Hardware
|
||||
@@ -42,7 +42,7 @@ jeder Spieler sein Spiel nach seinem eigenen Geschmack gestalten, während ander
|
||||
Einstellungen wählen können!
|
||||
|
||||
### Wo bekomme ich so eine YAML-Datei her?
|
||||
Die [Player Settings](/player-settings) Seite auf der Website ermöglicht das einfache Erstellen und Herunterladen
|
||||
Die [Player Settings](/games/A Link to the Past/player-settings) Seite auf der Website ermöglicht das einfache Erstellen und Herunterladen
|
||||
deiner eigenen `yaml` Datei. Drei verschiedene Voreinstellungen können dort gespeichert werden.
|
||||
|
||||
### Deine YAML-Datei ist gewichtet!
|
||||
@@ -74,12 +74,12 @@ bei der [YAML Validator](/mysterycheck) Seite tun.
|
||||
### Erhalte deine Patch-Datei und erstelle dein ROM
|
||||
Wenn du an einem MultiWorld-Spiel teilnehmen möchtest, wirst du in der Regel vom Host nach deiner YAML-Datei gefragt.
|
||||
Sobald du diese weitergegeben hast, wird der Host einen Link bereitstellen, wo du deinen Patch oder eine .zip-Datei
|
||||
mit allen Patches herunterladen kannst. Die Patch-Datei hat immer die Endung `.bmbp`.
|
||||
mit allen Patches herunterladen kannst. Die Patch-Datei hat immer die Endung `.apbp`.
|
||||
|
||||
### Mit dem Client verbinden
|
||||
|
||||
#### Via Emulator
|
||||
Wenn der client den Emulator automatisch gestartet hat, wird QUsb2Snes ebenfalls im Hintergrund gestartet.
|
||||
Wenn der client den Emulator automatisch gestartet hat, wird SNI ebenfalls im Hintergrund gestartet.
|
||||
Wenn dies das erste Mal ist, wird möglicherweise ein Fenster angezeigt, wo man bestätigen muss, dass das Programm
|
||||
durch die Windows Firewall kommunizieren darf.
|
||||
|
||||
@@ -88,8 +88,9 @@ durch die Windows Firewall kommunizieren darf.
|
||||
2. Klicke auf den Reiter "File" oben im Menü und wähle **Lua Scripting**
|
||||
3. Klicke auf **New Lua Script Window...**
|
||||
4. Im sich neu öffnenden Fenster, klicke auf **Browse...**
|
||||
5. Navigiere zum Ort, wo du snes9x Multitroid installiert hast, öffne den `lua`-Ordner und öffne `multibridge.lua`
|
||||
6. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
|
||||
5. Navigiere zum Verzeichnis, wo du Archipelago installiert hast und dort in den Unterordner `SNI`.
|
||||
6. Wähle dort die `Connector.lua` und klicke auf Öffnen.
|
||||
7. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
|
||||
"Snes Device: Connected" mit demselben Namen dort steht (in der oberen linken Ecke).
|
||||
|
||||
##### BizHawk
|
||||
@@ -99,9 +100,8 @@ durch die Windows Firewall kommunizieren darf.
|
||||
2. Lade die entsprechende ROM-Datei, wenn sie nicht schon automatisch geladen wurde.
|
||||
3. Klicke auf das Tools-Menü und klicke auf **Lua Console**
|
||||
4. Klicke auf den Button um ein neues Lua-Script zu öffnen.
|
||||
5. Navigiere zum Verzeichnis, wo du die Multiworld Utilities installiert hast und dort in folgende Ordner:
|
||||
`QUsb2Snes/Qusb2Snes/LuaBridge`
|
||||
6. Wähle dort die `luabridge.lua` und klicke auf Öffnen.
|
||||
5. Navigiere zum Verzeichnis, wo du Archipelago installiert hast und dort in den Unterordner `SNI`.
|
||||
6. Wähle dort die `Connector.lua` und klicke auf Öffnen.
|
||||
7. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
|
||||
"Snes Device: Connected" mit demselben Namen dort steht (in der oberen linken Ecke)
|
||||
|
||||
@@ -111,15 +111,11 @@ das noch nicht getan hast, so tue dies am besten jetzt! SD2SNES und FXPak Pro Nu
|
||||
[hier](https://github.com/RedGuyyyy/sd2snes/releases). Nutzer ähnlicher Hardware finden Hilfestellung
|
||||
[auf dieser Seite](http://usb2snes.com/#supported-platforms).
|
||||
|
||||
**UM MIT HARDWARE ZU VERBINDEN WIRD AKTUELL EINE ALTE VERSION VON QUSB2SNES BENÖTIGT
|
||||
([v0.7.16](https://github.com/Skarsnik/QUsb2snes/releases/tag/v0.7.16)).**
|
||||
Neuere Versionen funktionieren möglicherweise nur eingeschränkt, fehlerhaft oder gar nicht!
|
||||
|
||||
1. Schließe deinen Emulator, falls er automatisch gestartet haben sollte.
|
||||
2. Schließe QUsb2Snes, welches automatisch mit dem Client gestartet wurde (in der Taskleiste zu finden).
|
||||
3. Starte die richtige version von QUsb2Snes (v0.7.16).
|
||||
4. Starte deine (Original-)Konsole und lade die ROM-Datei.
|
||||
5. Schaue auf dein Clientfenster, welches nun "Snes Device: Connected" und den namen deiner Konsole
|
||||
2. Start SNI
|
||||
3. Starte deine (Original-)Konsole und lade die ROM-Datei.
|
||||
4. Schaue auf dein Clientfenster, welches nun "Snes Device: Connected" und den namen deiner Konsole
|
||||
zeigen sollte.
|
||||
|
||||
### Mit dem MultiServer verbinden
|
||||
@@ -137,7 +133,7 @@ können du und deine Freunde loslegen! Glückwunsch zum erfolgreichen Beitritt z
|
||||
|
||||
## Ein Multiworld-Spiel hosten
|
||||
Die Empfohlene Art, ein Spiel zu hosten, ist, den Service auf
|
||||
[der website](https://berserkermulti.world/generate) zu nutzen. Das Ganze ist recht einfach:
|
||||
[der website](/generate) zu nutzen. Das Ganze ist recht einfach:
|
||||
|
||||
1. Lasse dir von deinen Mitspielern die YAML-Datei zuschicken.
|
||||
2. Erstelle einen Zip-komprimierten Ordner´, in den du alle YAML-Dateien deiner Spieler einfügst.
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
## Required Software
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
|
||||
- [SNI](https://github.com/alttpo/sni/releases) (Included in Archipelago)
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of running Lua scripts
|
||||
- An emulator capable of connecting to SNI
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
[BizHawk](http://tasvideos.org/BizHawk.html))
|
||||
- An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware
|
||||
@@ -19,10 +19,9 @@
|
||||
## Installation Procedures
|
||||
|
||||
### Windows Setup
|
||||
1. Download and install the MultiWorld Utilities from the link above, making sure to install the most recent version.
|
||||
1. Download and install Archipelago from the link above, making sure to install the most recent version.
|
||||
**The file is located in the assets section at the bottom of the version information**. If you intend to play normal
|
||||
multiworld games, you want `Setup.Archipelago.exe`
|
||||
- If you intend to play the doors variant of multiworld, you will want to download the alternate doors file.
|
||||
- During the installation process, you will be asked to browse for your Japanese 1.0 ROM file. If you have
|
||||
installed this software before and are simply upgrading now, you will not be prompted to locate your
|
||||
ROM file a second time.
|
||||
@@ -50,7 +49,7 @@ each player to enjoy an experience customized for their taste, and different pla
|
||||
can all have different options.
|
||||
|
||||
### Where do I get a YAML file?
|
||||
The [Generate Game](/player-settings) page on the website allows you to configure your personal settings and
|
||||
The [Generate Game](/games/A Link to the Past/player-settings) page on the website allows you to configure your personal settings and
|
||||
export a YAML file from them.
|
||||
|
||||
### Verifying your YAML file
|
||||
@@ -68,7 +67,7 @@ If you would like to validate your YAML file to make sure it works, you may do s
|
||||
### Obtain your patch file and create your ROM
|
||||
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that
|
||||
is done, the host will provide you with either a link to download your patch file, or with a zip file containing
|
||||
everyone's patch files. Your patch file should have a `.bmbp` extension.
|
||||
everyone's patch files. Your patch file should have a `.apbp` extension.
|
||||
|
||||
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically
|
||||
launch the client, and will also create your ROM file in the same place as your patch file.
|
||||
@@ -76,7 +75,7 @@ launch the client, and will also create your ROM file in the same place as your
|
||||
### Connect to the client
|
||||
|
||||
#### With an emulator
|
||||
When the client launched automatically, QUsb2Snes should have also automatically launched in the background.
|
||||
When the client launched automatically, SNI should have also automatically launched in the background.
|
||||
If this is its first time launching, you may be prompted to allow it to communicate through the Windows
|
||||
Firewall.
|
||||
|
||||
@@ -98,8 +97,8 @@ Firewall.
|
||||
3. Click on the Tools menu and click on **Lua Console**
|
||||
4. Click the button to open a new Lua script.
|
||||
5. Browse to your MultiWorld Utilities installation directory, and into the following directories:
|
||||
`QUsb2Snes/Qusb2Snes/LuaBridge`
|
||||
6. Select `luabridge.lua` and click Open.
|
||||
`SNI`
|
||||
6. Select `Connector.lua` and click Open.
|
||||
7. Observe a name has been assigned to you, and that the client shows "SNES Device: Connected", with that same
|
||||
name in the upper left corner.
|
||||
|
||||
|
||||
@@ -118,3 +118,21 @@
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.markdown table{
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown table th{
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
border: 1px solid #eeffeb;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.markdown table td{
|
||||
text-align: left;
|
||||
border: 1px solid #eeffeb;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
136
WebHostLib/static/styles/ootTracker.css
Normal file
@@ -0,0 +1,136 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#inventory-table{
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 448px;
|
||||
background-color: rgb(60, 114, 157);
|
||||
}
|
||||
|
||||
#inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
filter: grayscale(100%) contrast(75%) brightness(30%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
filter: none;
|
||||
}
|
||||
|
||||
#inventory-table div.counted-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#inventory-table div.item-count {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
bottom: 0px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
width: 448px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: rgb(60, 114, 157);
|
||||
padding: 0 3px 3px;
|
||||
font-family: monospace;
|
||||
font-size: 15px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter {
|
||||
text-align: right;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#location-table td.toggle-arrow {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table tr#Total-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#location-table tbody.locations {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#location-table td.location-name {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-align {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table td:first-child {
|
||||
width: 272px;
|
||||
}
|
||||
|
||||
.location-category td:first-child {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#lullaby{
|
||||
filter: sepia(100%) hue-rotate(-60deg); /* css trick to hue-shift a static image */
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#epona{
|
||||
filter: sepia(100%) hue-rotate(-20deg) saturate(250%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#saria{
|
||||
filter: sepia(100%) hue-rotate(60deg) saturate(150%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#sun{
|
||||
filter: sepia(100%) hue-rotate(15deg) saturate(200%) brightness(120%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#time{
|
||||
filter: sepia(100%) hue-rotate(160deg) saturate(150%);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
#tutorial-wrapper{
|
||||
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;
|
||||
}
|
||||
|
||||
#tutorial-wrapper img{
|
||||
max-width: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#tutorial-wrapper p{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#tutorial-wrapper a{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
#tutorial-wrapper 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;
|
||||
}
|
||||
|
||||
#tutorial-wrapper 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;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h3{
|
||||
font-size: 1.70rem;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h4{
|
||||
font-size: 1.5rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h5{
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h6{
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
color: #434343;
|
||||
}
|
||||
|
||||
#tutorial-wrapper h3, #tutorial-wrapper h4, #tutorial-wrapper h5,#tutorial-wrapper h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#tutorial-wrapper ul{
|
||||
|
||||
}
|
||||
|
||||
#tutorial-wrapper ol{
|
||||
|
||||
}
|
||||
|
||||
#tutorial-wrapper li{
|
||||
|
||||
}
|
||||
|
||||
#tutorial-wrapper pre{
|
||||
margin-top: 0;
|
||||
padding: 0.5rem 0.25rem;
|
||||
background-color: #ffeeab;
|
||||
border: 1px solid #9f916a;
|
||||
border-radius: 6px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#tutorial-wrapper code{
|
||||
background-color: #ffeeab;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#tutorial-wrapper #tutorial-video-container{
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#tutorial-wrapper #language-selector-wrapper{
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
17
WebHostLib/templates/gameInfo.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ game }} Info</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/gameInfo.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="game-info" class="markdown" data-lang="{{ lang }}" data-game="{{ game }}">
|
||||
<!-- Populated my JS / MD -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>A Link to the Past</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/zelda3/zelda3.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>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="zelda3">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Factorio</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/factorio/factorio.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>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="factorio">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Minecraft</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/minecraft/minecraft.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>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="minecraft">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Subnautica</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/subnautica/subnautica.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>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="subnautica">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -20,6 +20,7 @@
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Amount</th>
|
||||
<th>Order Received</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -28,6 +29,7 @@
|
||||
<tr>
|
||||
<td>{{ name | item_name }}</td>
|
||||
<td>{{ count }}</td>
|
||||
<td>{{received_items[name]}}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
|
||||
|
||||
180
WebHostLib/templates/ootTracker.html
Normal file
@@ -0,0 +1,180 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/ootTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/ootTracker.js') }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ ocarina_url }}" class="{{ 'acquired' if 'Ocarina' in acquired_items }}" title="Ocarina" /></td>
|
||||
<td><img src="{{ icons['Bombs'] }}" class="{{ 'acquired' if 'Bomb Bag' in acquired_items }}" title="Bombs" /></td>
|
||||
<td><img src="{{ icons['Bow'] }}" class="{{ 'acquired' if 'Bow' in acquired_items }}" title="Fairy Bow" /></td>
|
||||
<td><img src="{{ icons['Fire Arrows'] }}" class="{{ 'acquired' if 'Fire Arrows' in acquired_items }}" title="Fire Arrows" /></td>
|
||||
<td><img src="{{ icons['Kokiri Sword'] }}" class="{{ 'acquired' if 'Kokiri Sword' in acquired_items }}" title="Kokiri Sword" /></td>
|
||||
<td><img src="{{ icons['Biggoron Sword'] }}" class="{{ 'acquired' if 'Biggoron Sword' in acquired_items }}" title="Biggoron's Sword" /></td>
|
||||
<td><img src="{{ icons['Mirror Shield'] }}" class="{{ 'acquired' if 'Mirror Shield' in acquired_items }}" title="Mirror Shield" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Slingshot'] }}" class="{{ 'acquired' if 'Slingshot' in acquired_items }}" title="Slingshot" /></td>
|
||||
<td><img src="{{ icons['Bombchus'] }}" class="{{ 'acquired' if has_bombchus }}" title="Bombchus" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ hookshot_url }}" class="{{ 'acquired' if 'Progressive Hookshot' in acquired_items }}" title="Progressive Hookshot" />
|
||||
<div class="item-count">{{ hookshot_length }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ icons['Ice Arrows'] }}" class="{{ 'acquired' if 'Ice Arrows' in acquired_items }}" title="Ice Arrows" /></td>
|
||||
<td><img src="{{ strength_upgrade_url }}" class="{{ 'acquired' if 'Progressive Strength Upgrade' in acquired_items }}" title="Progressive Strength Upgrade" /></td>
|
||||
<td><img src="{{ icons['Goron Tunic'] }}" class="{{ 'acquired' if 'Goron Tunic' in acquired_items }}" title="Goron Tunic" /></td>
|
||||
<td><img src="{{ icons['Zora Tunic'] }}" class="{{ 'acquired' if 'Zora Tunic' in acquired_items }}" title="Zora Tunic" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Boomerang'] }}" class="{{ 'acquired' if 'Boomerang' in acquired_items }}" title="Boomerang" /></td>
|
||||
<td><img src="{{ icons['Lens of Truth'] }}" class="{{ 'acquired' if 'Lens of Truth' in acquired_items }}" title="Lens of Truth" /></td>
|
||||
<td><img src="{{ icons['Megaton Hammer'] }}" class="{{ 'acquired' if 'Megaton Hammer' in acquired_items }}" title="Megaton Hammer" /></td>
|
||||
<td><img src="{{ icons['Light Arrows'] }}" class="{{ 'acquired' if 'Light Arrows' in acquired_items }}" title="Light Arrows" /></td>
|
||||
<td><img src="{{ scale_url }}" class="{{ 'acquired' if 'Progressive Scale' in acquired_items }}" title="Progressive Scale" /></td>
|
||||
<td><img src="{{ icons['Iron Boots'] }}" class="{{ 'acquired' if 'Iron Boots' in acquired_items }}" title="Iron Boots" /></td>
|
||||
<td><img src="{{ icons['Hover Boots'] }}" class="{{ 'acquired' if 'Hover Boots' in acquired_items }}" title="Hover Boots" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ bottle_url }}" class="{{ 'acquired' if bottle_count > 0 }}" title="Bottles" />
|
||||
<div class="item-count">{{ bottle_count if bottle_count > 0 else '' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ icons['Dins Fire'] }}" class="{{ 'acquired' if 'Dins Fire' in acquired_items }}" title="Din's Fire" /></td>
|
||||
<td><img src="{{ icons['Farores Wind'] }}" class="{{ 'acquired' if 'Farores Wind' in acquired_items }}" title="Farore's Wind" /></td>
|
||||
<td><img src="{{ icons['Nayrus Love'] }}" class="{{ 'acquired' if 'Nayrus Love' in acquired_items }}" title="Nayru's Love" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ wallet_url }}" class="{{ 'acquired' if 'Progressive Wallet' in acquired_items }}" title="Progressive Wallet" />
|
||||
<div class="item-count">{{ wallet_size }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ magic_meter_url }}" class="{{ 'acquired' if 'Magic Meter' in acquired_items }}" title="Magic Meter" /></td>
|
||||
<td><img src="{{ icons['Gerudo Membership Card'] }}" class="{{ 'acquired' if 'Gerudo Membership Card' in acquired_items }}" title="Gerudo Membership Card" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Zeldas Lullaby'] }}" class="{{ 'acquired' if 'Zeldas Lullaby' in acquired_items }}" title="Zelda's Lullaby" id="lullaby"/>
|
||||
<div class="item-count">Zelda</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Eponas Song'] }}" class="{{ 'acquired' if 'Eponas Song' in acquired_items }}" title="Epona's Song" id="epona" />
|
||||
<div class="item-count">Epona</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Sarias Song'] }}" class="{{ 'acquired' if 'Sarias Song' in acquired_items }}" title="Saria's Song" id="saria"/>
|
||||
<div class="item-count">Saria</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Suns Song'] }}" class="{{ 'acquired' if 'Suns Song' in acquired_items }}" title="Sun's Song" id="sun"/>
|
||||
<div class="item-count">Sun</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Song of Time'] }}" class="{{ 'acquired' if 'Song of Time' in acquired_items }}" title="Song of Time" id="time"/>
|
||||
<div class="item-count">Time</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Song of Storms'] }}" class="{{ 'acquired' if 'Song of Storms' in acquired_items }}" title="Song of Storms" />
|
||||
<div class="item-count">Storms</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Gold Skulltula Token'] }}" class="{{ 'acquired' if token_count > 0 }}" title="Gold Skulltula Tokens" />
|
||||
<div class="item-count">{{ token_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Minuet of Forest'] }}" class="{{ 'acquired' if 'Minuet of Forest' in acquired_items }}" title="Minuet of Forest" />
|
||||
<div class="item-count">Min</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Bolero of Fire'] }}" class="{{ 'acquired' if 'Bolero of Fire' in acquired_items }}" title="Bolero of Fire" />
|
||||
<div class="item-count">Bol</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Serenade of Water'] }}" class="{{ 'acquired' if 'Serenade of Water' in acquired_items }}" title="Serenade of Water" />
|
||||
<div class="item-count">Ser</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Requiem of Spirit'] }}" class="{{ 'acquired' if 'Requiem of Spirit' in acquired_items }}" title="Requiem of Spirit" />
|
||||
<div class="item-count">Req</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Nocturne of Shadow'] }}" class="{{ 'acquired' if 'Nocturne of Shadow' in acquired_items }}" title="Nocturne of Shadow" />
|
||||
<div class="item-count">Noc</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Prelude of Light'] }}" class="{{ 'acquired' if 'Prelude of Light' in acquired_items }}" title="Prelude of Light" />
|
||||
<div class="item-count">Pre</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Triforce'] if game_finished else icons['Triforce Piece'] }}" class="{{ 'acquired' if game_finished or piece_count > 0 }}" title="{{ 'Triforce' if game_finished else 'Triforce Pieces' }}" id=triforce />
|
||||
<div class="item-count">{{ piece_count if piece_count > 0 else '' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Small Key'] }}" title="Small Keys" /></td>
|
||||
<td><img src="{{ icons['Boss Key'] }}" title="Boss Key" /></td>
|
||||
<td class="right-align">Items</td>
|
||||
</tr>
|
||||
{% for area in checks_done %}
|
||||
<tr class="location-category" id="{{area}}-header">
|
||||
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||
<td class="smallkeys">{{ small_key_counts.get(area, '-') }}</td>
|
||||
<td class="bosskeys">{{ boss_key_counts.get(area, '-') }}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
</tr>
|
||||
<tbody class="locations hide" id="{{area}}">
|
||||
{% for location in location_info[area] %}
|
||||
<tr>
|
||||
<td class="location-name">{{ location }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,22 +36,7 @@ accessibility:
|
||||
progression_balancing:
|
||||
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
|
||||
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
|
||||
# The following 4 options can be uncommented and moved into a game's section they should affect
|
||||
# start_inventory: # Begin the file with the listed items/upgrades
|
||||
# Please only use items for the correct game, use triggers if need to be have seperated lists.
|
||||
# Pegasus Boots: on
|
||||
# Bomb Upgrade (+10): 4
|
||||
# Arrow Upgrade (+10): 4
|
||||
# start_hints: # Begin the game with these items' locations revealed to you at the start of the game. Get the info via !hint in your client.
|
||||
# - Moon Pearl
|
||||
# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
|
||||
# - "Moon Pearl"
|
||||
# - "Small Keys"
|
||||
# - "Big Keys"
|
||||
# non_local_items: # Force certain items to appear outside your world only, unless in single-player. Recognizes some group names, like "Swords"
|
||||
# - "Progressive Weapons"
|
||||
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
|
||||
# - "Master Sword Pedestal"
|
||||
|
||||
{%- macro range_option(option) %}
|
||||
# you can add additional values between minimum and maximum
|
||||
{%- set data, notes = dictify_range(option) %}
|
||||
@@ -62,14 +47,14 @@ progression_balancing:
|
||||
{{ 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 %}
|
||||
{%- if option.range_start is defined and option.range_start is number %}
|
||||
{{- range_option(option) -}}
|
||||
{%- elif option.options -%}
|
||||
{%- 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) }}
|
||||
{{ yaml_dump(default_converter(option.default)) | indent(4, first=False) }}
|
||||
{%- endif -%}
|
||||
{%- endfor %}
|
||||
{% if not options %}{}{% endif %}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<title>{{ game }} Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-settings.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/md5.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/player-settings.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div id="games">
|
||||
<h1>Currently Supported Games</h1>
|
||||
{% for game, description in worlds.items() %}
|
||||
<h3><a href="{{ url_for("game_page", game=game) }}/player-settings">{{ game }}</a></h3>
|
||||
<h3><a href="{{ url_for("player_settings", game=game) }}">{{ game }}</a></h3>
|
||||
<p>{{ description }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block head %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<title>Archipelago</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tutorial.css") }}" />
|
||||
<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>
|
||||
@@ -11,7 +11,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="tutorial-wrapper" data-game="{{ game }}" data-file="{{ file }}" data-lang="{{ lang }}">
|
||||
<div id="tutorial-wrapper" class="markdown" data-game="{{ game }}" data-file="{{ file }}" data-lang="{{ lang }}">
|
||||
<!-- Content generated by JavaScript -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,6 +10,7 @@ from WebHostLib import app, cache, Room
|
||||
from Utils import restricted_loads
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
|
||||
|
||||
def get_alttp_id(item_name):
|
||||
return Items.item_table[item_name][2]
|
||||
|
||||
@@ -283,6 +284,7 @@ def render_timedelta(delta: datetime.timedelta):
|
||||
|
||||
_multidata_cache = {}
|
||||
|
||||
|
||||
def get_location_table(checks_table: dict) -> dict:
|
||||
loc_to_area = {}
|
||||
for area, locations in checks_table.items():
|
||||
@@ -292,6 +294,7 @@ def get_location_table(checks_table: dict) -> dict:
|
||||
loc_to_area[location] = area
|
||||
return loc_to_area
|
||||
|
||||
|
||||
def get_static_room_data(room: Room):
|
||||
result = _multidata_cache.get(room.seed.id, None)
|
||||
if result:
|
||||
@@ -311,7 +314,7 @@ def get_static_room_data(room: Room):
|
||||
seed_checks_in_area["Total"] = 249
|
||||
|
||||
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][playernumber][areaname])
|
||||
if areaname != "Total" else multidata["checks_in_area"][playernumber]["Total"]
|
||||
if areaname != "Total" else multidata["checks_in_area"][playernumber]["Total"]
|
||||
for areaname in ordered_areas}
|
||||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
|
||||
@@ -373,9 +376,9 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
for location in locations_checked:
|
||||
if location in player_locations:
|
||||
item, recipient = player_locations[location]
|
||||
if recipient == tracked_player: # a check done for the tracked player
|
||||
if recipient == tracked_player: # a check done for the tracked player
|
||||
attribute_item_solo(inventory, item)
|
||||
if ms_player == tracked_player: # a check done by the tracked player
|
||||
if ms_player == tracked_player: # a check done by the tracked player
|
||||
checks_done[location_to_area[location]] += 1
|
||||
checks_done["Total"] += 1
|
||||
if games[tracked_player] == "A Link to the Past":
|
||||
@@ -403,28 +406,28 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
# Determine which icon to use
|
||||
display_data = {}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name])-1)
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
acquired = True
|
||||
if not display_name:
|
||||
acquired = False
|
||||
display_name = progressive_names[item_name][level+1]
|
||||
display_name = progressive_names[item_name][level + 1]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower()
|
||||
display_data[base_name+"_acquired"] = acquired
|
||||
display_data[base_name+"_url"] = icons[display_name]
|
||||
|
||||
display_data[base_name + "_acquired"] = acquired
|
||||
display_data[base_name + "_url"] = icons[display_name]
|
||||
|
||||
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
|
||||
sp_areas = ordered_areas[2:15]
|
||||
|
||||
return render_template("lttpTracker.html", inventory=inventory,
|
||||
player_name=player_name, room=room, icons=icons, checks_done=checks_done,
|
||||
checks_in_area=seed_checks_in_area[tracked_player], acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
checks_in_area=seed_checks_in_area[tracked_player],
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
|
||||
key_locations=player_small_key_locations[tracked_player],
|
||||
big_key_locations=player_big_key_locations[tracked_player],
|
||||
**display_data)
|
||||
elif games[tracked_player] == "Minecraft":
|
||||
elif games[tracked_player] == "Minecraft":
|
||||
minecraft_icons = {
|
||||
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
|
||||
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
|
||||
@@ -455,14 +458,14 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
}
|
||||
|
||||
minecraft_location_ids = {
|
||||
"Story": [42073, 42080, 42081, 42023, 42082, 42027, 42039, 42085, 42002, 42009, 42010,
|
||||
"Story": [42073, 42080, 42081, 42023, 42082, 42027, 42039, 42085, 42002, 42009, 42010,
|
||||
42070, 42041, 42049, 42090, 42004, 42031, 42025, 42029, 42051, 42077, 42089],
|
||||
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
|
||||
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
|
||||
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014],
|
||||
"The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
|
||||
"Adventure": [42047, 42086, 42087, 42050, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
|
||||
"Adventure": [42047, 42086, 42087, 42050, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
|
||||
42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42088],
|
||||
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028,
|
||||
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028,
|
||||
42036, 42057, 42063, 42053, 42083, 42084, 42091]
|
||||
}
|
||||
|
||||
@@ -482,10 +485,10 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
|
||||
}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name])-1)
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||
display_data[base_name+"_url"] = minecraft_icons[display_name]
|
||||
display_data[base_name + "_url"] = minecraft_icons[display_name]
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
@@ -496,7 +499,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
if count >= 0:
|
||||
display_data[base_name+"_count"] = count
|
||||
display_data[base_name + "_count"] = count
|
||||
|
||||
# Victory condition
|
||||
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
|
||||
@@ -505,26 +508,214 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
# Turn location IDs into advancement tab counts
|
||||
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
|
||||
lookup_name = lambda id: lookup_any_location_id_to_name[id]
|
||||
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done['Total'] = len(checked_locations)
|
||||
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
return render_template("minecraftTracker.html",
|
||||
inventory=inventory, icons=minecraft_icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name},
|
||||
return render_template("minecraftTracker.html",
|
||||
inventory=inventory, icons=minecraft_icons,
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||
id in lookup_any_item_id_to_name},
|
||||
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
|
||||
elif games[tracked_player] == "Ocarina of Time":
|
||||
oot_icons = {
|
||||
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
|
||||
"Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png",
|
||||
"Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png",
|
||||
"Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png",
|
||||
"Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png",
|
||||
"Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png",
|
||||
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png",
|
||||
"Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png",
|
||||
"Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png",
|
||||
"Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png",
|
||||
"Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png",
|
||||
"Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png",
|
||||
"Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png",
|
||||
"Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png",
|
||||
"Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png",
|
||||
"Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png",
|
||||
"Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png",
|
||||
"Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png",
|
||||
"Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png",
|
||||
"Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png",
|
||||
"Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png",
|
||||
"Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png",
|
||||
"Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png",
|
||||
"Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png",
|
||||
"Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png",
|
||||
"Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png",
|
||||
"Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png",
|
||||
"Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png",
|
||||
"Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png",
|
||||
"Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png",
|
||||
"Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png",
|
||||
"Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png",
|
||||
"Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png",
|
||||
"Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png",
|
||||
"Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png",
|
||||
"Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png",
|
||||
"Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png",
|
||||
"Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png",
|
||||
"Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png",
|
||||
"Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png",
|
||||
"Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png",
|
||||
"Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png",
|
||||
"Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png",
|
||||
"Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png",
|
||||
"Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png",
|
||||
"Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png",
|
||||
"Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png",
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Determine display for progressive items
|
||||
progressive_items = {
|
||||
"Progressive Hookshot": 66128,
|
||||
"Progressive Strength Upgrade": 66129,
|
||||
"Progressive Wallet": 66133,
|
||||
"Progressive Scale": 66134,
|
||||
"Magic Meter": 66138,
|
||||
"Ocarina": 66139,
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"],
|
||||
"Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", "Golden Gauntlets"],
|
||||
"Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"],
|
||||
"Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"],
|
||||
"Magic Meter": ["Small Magic", "Small Magic", "Large Magic"],
|
||||
"Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"]
|
||||
}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name])-1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
if item_name.startswith("Progressive"):
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||
else:
|
||||
base_name = item_name.lower().replace(' ', '_')
|
||||
display_data[base_name+"_url"] = oot_icons[display_name]
|
||||
|
||||
if base_name == "hookshot":
|
||||
display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
|
||||
if base_name == "wallet":
|
||||
display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
|
||||
|
||||
# Determine display for bottles. Show letter if it's obtained, determine bottle count
|
||||
bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148]
|
||||
display_data['bottle_count'] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4)
|
||||
display_data['bottle_url'] = oot_icons['Rutos Letter'] if inventory[66021] > 0 else oot_icons['Bottle']
|
||||
|
||||
# Determine bombchu display
|
||||
display_data['has_bombchus'] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137]))
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
"Gold Skulltula Token": 66091,
|
||||
"Triforce Piece": 66202,
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
display_data[base_name+"_count"] = inventory[item_id]
|
||||
|
||||
# Gather dungeon locations
|
||||
area_id_ranges = {
|
||||
"Overworld": (67000, 67280),
|
||||
"Deku Tree": (67281, 67303),
|
||||
"Dodongo's Cavern": (67304, 67334),
|
||||
"Jabu Jabu's Belly": (67335, 67359),
|
||||
"Bottom of the Well": (67360, 67384),
|
||||
"Forest Temple": (67385, 67420),
|
||||
"Fire Temple": (67421, 67457),
|
||||
"Water Temple": (67458, 67484),
|
||||
"Shadow Temple": (67485, 67532),
|
||||
"Spirit Temple": (67533, 67582),
|
||||
"Ice Cavern": (67583, 67596),
|
||||
"Gerudo Training Grounds": (67597, 67635),
|
||||
"Ganon's Castle": (67636, 67673),
|
||||
}
|
||||
def lookup_and_trim(id, area):
|
||||
full_name = lookup_any_location_id_to_name[id]
|
||||
if id == 67673:
|
||||
return full_name[13:] # Ganons Tower Boss Key Chest
|
||||
if area != 'Overworld':
|
||||
return full_name[len(area):] # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC
|
||||
return full_name
|
||||
|
||||
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set()).intersection(set(locations[tracked_player]))
|
||||
location_info = {area: {lookup_and_trim(id, area): id in checked_locations for id in range(min_id, max_id+1) if id in locations[tracked_player]}
|
||||
for area, (min_id, max_id) in area_id_ranges.items()}
|
||||
checks_done = {area: len(list(filter(lambda x: x, location_info[area].values()))) for area in area_id_ranges}
|
||||
checks_in_area = {area: len([id for id in range(min_id, max_id+1) if id in locations[tracked_player]])
|
||||
for area, (min_id, max_id) in area_id_ranges.items()}
|
||||
checks_done['Total'] = sum(checks_done.values())
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
# Give skulltulas on non-tracked locations
|
||||
non_tracked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set()).difference(set(locations[tracked_player]))
|
||||
for id in non_tracked_locations:
|
||||
if "GS" in lookup_and_trim(id, ''):
|
||||
display_data["token_count"] += 1
|
||||
|
||||
# Gather small and boss key info
|
||||
small_key_counts = {
|
||||
"Forest Temple": inventory[66175],
|
||||
"Fire Temple": inventory[66176],
|
||||
"Water Temple": inventory[66177],
|
||||
"Spirit Temple": inventory[66178],
|
||||
"Shadow Temple": inventory[66179],
|
||||
"Bottom of the Well": inventory[66180],
|
||||
"Gerudo Training Grounds": inventory[66181],
|
||||
"Ganon's Castle": inventory[66183],
|
||||
}
|
||||
boss_key_counts = {
|
||||
"Forest Temple": '✔' if inventory[66149] else '✕',
|
||||
"Fire Temple": '✔' if inventory[66150] else '✕',
|
||||
"Water Temple": '✔' if inventory[66151] else '✕',
|
||||
"Spirit Temple": '✔' if inventory[66152] else '✕',
|
||||
"Shadow Temple": '✔' if inventory[66153] else '✕',
|
||||
"Ganon's Castle": '✔' if inventory[66154] else '✕',
|
||||
}
|
||||
|
||||
# Victory condition
|
||||
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
|
||||
display_data['game_finished'] = game_state == 30
|
||||
|
||||
return render_template("ootTracker.html",
|
||||
inventory=inventory, player=tracked_player, team=tracked_team, room=room, player_name=player_name,
|
||||
icons=oot_icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
small_key_counts=small_key_counts, boss_key_counts=boss_key_counts,
|
||||
**display_data)
|
||||
|
||||
else:
|
||||
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
|
||||
player_received_items = {}
|
||||
for order_index, networkItem in enumerate(
|
||||
multisave.get('received_items', {}).get((tracked_team, tracked_player), []),
|
||||
start=1
|
||||
):
|
||||
player_received_items[networkItem.item] = order_index
|
||||
return render_template("genericTracker.html",
|
||||
inventory=inventory,
|
||||
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
|
||||
checked_locations= checked_locations, not_checked_locations = set(locations[tracked_player])-checked_locations)
|
||||
checked_locations=checked_locations,
|
||||
not_checked_locations=set(locations[tracked_player]) - checked_locations,
|
||||
received_items=player_received_items)
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>')
|
||||
@@ -602,4 +793,4 @@ def getTracker(tracker: UUID):
|
||||
checks_in_area=seed_checks_in_area, activity_timers=activity_timers,
|
||||
key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids,
|
||||
video=video, big_key_locations=group_big_key_locations,
|
||||
hints=hints, long_player_names = long_player_names)
|
||||
hints=hints, long_player_names=long_player_names)
|
||||
|
||||
@@ -3,6 +3,7 @@ import lzma
|
||||
import json
|
||||
import base64
|
||||
import MultiServer
|
||||
import uuid
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
from pony.orm import flush, select
|
||||
@@ -17,6 +18,68 @@ accepted_zip_contents = {"patches": ".apbp",
|
||||
banned_zip_contents = (".sfc",)
|
||||
|
||||
|
||||
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
|
||||
if not owner:
|
||||
owner = session["_id"]
|
||||
infolist = zfile.infolist()
|
||||
slots = set()
|
||||
spoiler = ""
|
||||
multidata = None
|
||||
for file in infolist:
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
||||
"Your file was deleted."
|
||||
elif file.filename.endswith(".apbp"):
|
||||
data = zfile.open(file, "r").read()
|
||||
yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig"))
|
||||
if yaml_data["version"] < 2:
|
||||
return "Old format cannot be uploaded (outdated .apbp)", 500
|
||||
metadata = yaml_data["meta"]
|
||||
slots.add(Slot(data=data, player_name=metadata["player_name"],
|
||||
player_id=metadata["player_id"],
|
||||
game="A Link to the Past"))
|
||||
|
||||
elif file.filename.endswith(".apmc"):
|
||||
data = zfile.open(file, "r").read()
|
||||
metadata = json.loads(base64.b64decode(data).decode("utf-8"))
|
||||
slots.add(Slot(data=data, player_name=metadata["player_name"],
|
||||
player_id=metadata["player_id"],
|
||||
game="Minecraft"))
|
||||
|
||||
elif file.filename.endswith(".zip"):
|
||||
# Factorio mods need 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"):
|
||||
try:
|
||||
multidata = zfile.open(file).read()
|
||||
MultiServer.Context._decompress(multidata)
|
||||
except:
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
else:
|
||||
multidata = zfile.open(file).read()
|
||||
if multidata:
|
||||
flush() # commit slots
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta),
|
||||
id=sid if sid else uuid.uuid4())
|
||||
flush() # create seed
|
||||
for slot in slots:
|
||||
slot.seed = seed
|
||||
return seed
|
||||
else:
|
||||
flash("No multidata was found in the zip file, which is required.")
|
||||
|
||||
|
||||
@app.route('/uploads', methods=['GET', 'POST'])
|
||||
def uploads():
|
||||
if request.method == 'POST':
|
||||
@@ -31,64 +94,12 @@ def uploads():
|
||||
flash('No selected file')
|
||||
elif file and allowed_file(file.filename):
|
||||
if file.filename.endswith(".zip"):
|
||||
slots = set()
|
||||
spoiler = ""
|
||||
multidata = None
|
||||
with zipfile.ZipFile(file, 'r') as zfile:
|
||||
infolist = zfile.infolist()
|
||||
|
||||
for file in infolist:
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
||||
elif file.filename.endswith(".apbp"):
|
||||
data = zfile.open(file, "r").read()
|
||||
yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig"))
|
||||
if yaml_data["version"] < 2:
|
||||
return "Old format cannot be uploaded (outdated .apbp)", 500
|
||||
metadata = yaml_data["meta"]
|
||||
slots.add(Slot(data=data, player_name=metadata["player_name"],
|
||||
player_id=metadata["player_id"],
|
||||
game="A Link to the Past"))
|
||||
|
||||
elif file.filename.endswith(".apmc"):
|
||||
data = zfile.open(file, "r").read()
|
||||
metadata = json.loads(base64.b64decode(data).decode("utf-8"))
|
||||
slots.add(Slot(data=data, player_name=metadata["player_name"],
|
||||
player_id=metadata["player_id"],
|
||||
game="Minecraft"))
|
||||
|
||||
elif file.filename.endswith(".zip"):
|
||||
# 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"):
|
||||
try:
|
||||
multidata = zfile.open(file).read()
|
||||
MultiServer.Context._decompress(multidata)
|
||||
except:
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
else:
|
||||
multidata = zfile.open(file).read()
|
||||
if multidata:
|
||||
flush() # commit slots
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=session["_id"])
|
||||
flush() # create seed
|
||||
for slot in slots:
|
||||
slot.seed = seed
|
||||
|
||||
return redirect(url_for("viewSeed", seed=seed.id))
|
||||
else:
|
||||
flash("No multidata was found in the zip file, which is required.")
|
||||
res = upload_zip_to_db(zfile)
|
||||
if type(res) == str:
|
||||
return res
|
||||
elif res:
|
||||
return redirect(url_for("viewSeed", seed=res.id))
|
||||
else:
|
||||
try:
|
||||
multidata = file.read()
|
||||
|
||||
342
docs/adding games.md
Normal file
@@ -0,0 +1,342 @@
|
||||
|
||||
|
||||
# How do I add a game to Archipelago?
|
||||
This guide is going to try and be a broad summary of how you can do just that.
|
||||
There are three key steps to incorporating a game into Archipelago:
|
||||
- Game Modification
|
||||
- Archipelago Server Integration
|
||||
|
||||
# Game Modification
|
||||
One half of the work required to integrate a game into Archipelago is the development of the game client. This is
|
||||
typically done through a modding API or other modification process, described further down.
|
||||
|
||||
As an example, modifications to a game typically include (more on this later):
|
||||
- Hooking into when a 'location check' is completed.
|
||||
- Networking with the Archipelago server.
|
||||
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
|
||||
|
||||
In order to determine how to modify a game, refer to the following sections.
|
||||
|
||||
## Engine Identification
|
||||
This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
|
||||
Examples are provided below.
|
||||
|
||||
### Creepy Castle
|
||||

|
||||
|
||||
This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. It’s also your worst-case
|
||||
scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have
|
||||
basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty nasty
|
||||
disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other examples
|
||||
of game releases.
|
||||
|
||||
### Heavy Bullets
|
||||

|
||||
|
||||
Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
|
||||
“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
|
||||
with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
|
||||
information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never
|
||||
hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
|
||||
“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam.
|
||||
The directory “HEAVY_BULLETS_Data”, however, has some good news.
|
||||
|
||||

|
||||
|
||||
Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that
|
||||
what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which affirm
|
||||
our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered, extension-less
|
||||
level files and the sharedassets files. We’ll tell you a bit about why seeing a Unity game is such good news later,
|
||||
but for now, this is what one looks like. Also keep your eyes out for an executable with a name like UnityCrashHandler,
|
||||
that’s another dead giveaway.
|
||||
|
||||
### Stardew Valley
|
||||

|
||||
|
||||
This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
|
||||
Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good news.
|
||||
More on that later.
|
||||
|
||||
### Gato Roboto
|
||||

|
||||
|
||||
Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for.
|
||||
The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker.
|
||||
|
||||
This isn't all you'll ever see looking at game files, but it's a good place to start.
|
||||
As a general rule, the more files a game has out in plain sight, the more you'll be able to change.
|
||||
This especially applies in the case of code or script files - always keep a lookout for anything you can use to your
|
||||
advantage!
|
||||
|
||||
## Open or Leaked Source Games
|
||||
As a side note, many games have either been made open source, or have had source files leaked at some point.
|
||||
This can be a boon to any would-be modder, for obvious reasons.
|
||||
Always be sure to check - a quick internet search for "(Game) Source Code" might not give results often, but when it
|
||||
does you're going to have a much better time.
|
||||
|
||||
Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do
|
||||
so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical.
|
||||
|
||||
## Modifying Release Versions of Games
|
||||
However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install directory.
|
||||
Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools,
|
||||
but these are often not geared to the kind of work you'll be doing and may not help much.
|
||||
|
||||
As a general rule, any modding tool that lets you write actual code is something worth using.
|
||||
|
||||
### Research
|
||||
The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
|
||||
it's possible other motivated parties have concocted useful tools for your game already.
|
||||
Always be sure to search the Internet for the efforts of other modders.
|
||||
|
||||
### Analysis Tools
|
||||
Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to existing game tools.
|
||||
|
||||
#### [dnSpy](https://github.com/dnSpy/dnSpy/releases)
|
||||
The first tool in your toolbox is dnSpy.
|
||||
dnSpy is useful for opening and modifying code files, like .exe and .dll files, that were made in C#.
|
||||
This won't work for executable files made by other means, and obfuscated code (code which was deliberately made
|
||||
difficult to reverse engineer) will thwart it, but 9 times out of 10 this is exactly what you need.
|
||||
You'll want to avoid opening common library files in dnSpy, as these are unlikely to contain the data you're looking to
|
||||
modify.
|
||||
|
||||
For Unity games, the file you’ll want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as pictured below:
|
||||
|
||||

|
||||
|
||||
This file will contain the data of the actual game.
|
||||
For other C# games, the file you want is usually just the executable itself.
|
||||
|
||||
With dnSpy, you can view the game’s C# code, but the tool isn’t perfect.
|
||||
Although the names of classes, methods, variables, and more will be preserved, code structures may not remain entirely intact. This is because compilers will often subtly rewrite code to be more optimal, so that it works the same as the original code but uses fewer resources. Compiled C# files also lose comments and other documentation.
|
||||
|
||||
#### [UndertaleModTool](https://github.com/krzys-h/UndertaleModTool/releases)
|
||||
This is currently the best tool for modifying games made in GameMaker, and supports games made in both GMS 1 and 2.
|
||||
It allows you to modify code in GML, if the game wasn't made with the wrong compiler (usually something you don't have
|
||||
to worry about).
|
||||
|
||||
You'll want to open the data.win file, as this is where all the goods are kept.
|
||||
Like dnSpy, you won’t be able to see comments.
|
||||
In addition, you will be able to see and modify many hidden fields on items that GameMaker itself will often hide from
|
||||
creators.
|
||||
|
||||
Fonts in particular are notoriously complex, and to add new sprites you may need to modify existing sprite sheets.
|
||||
|
||||
#### [CheatEngine](https://cheatengine.org/)
|
||||
CheatEngine is a tool with a very long and storied history.
|
||||
Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
|
||||
malware (because this behavior is most commonly found in malware and rarely used by other programs).
|
||||
If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
|
||||
including binary data formats, addressing, and assembly language programming.
|
||||
|
||||
The tool itself is highly complex and even I have not yet charted its expanses.
|
||||
However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever
|
||||
modifying the actual game itself.
|
||||
In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
|
||||
anything with it.
|
||||
|
||||
### What Modifications You Should Make to the Game
|
||||
We talked about this briefly in [Game Modification](#game-modification) section.
|
||||
The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
|
||||
- Modify the game so that checks are shuffled
|
||||
- Know when the player has completed a check, and react accordingly
|
||||
- Listen for messages from the Archipelago server
|
||||
- Modify the game to display messages from the Archipelago server
|
||||
- Add interface for connecting to the Archipelago server with passwords and sessions
|
||||
- Add commands for manually rewarding, re-syncing, forfeiting, and other actions
|
||||
|
||||
To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive
|
||||
from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary,
|
||||
avoid double-awarding items while still maintaining game file integrity, and allow players to manually enter commands in
|
||||
case the client or server make mistakes.
|
||||
|
||||
Refer to the [Network Protocol documentation](./network%20protocol.md) for how to communicate with Archipelago's servers.
|
||||
|
||||
## But my Game is a console game. Can I still add it?
|
||||
That depends – what console?
|
||||
|
||||
### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
|
||||
Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
|
||||
that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
|
||||
holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console games.
|
||||
|
||||
### My Game isn’t that old, it’s for the Wii/PS2/360/etc
|
||||
This is very complex, but doable.
|
||||
If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
|
||||
There exist many disassembly and debugging tools, but more recent content may have lackluster support.
|
||||
|
||||
### My Game is a classic for the SNES/Sega Genesis/etc
|
||||
That’s a lot more feasible.
|
||||
There are many good tools available for understanding and modifying games on these older consoles, and the emulation
|
||||
community will have figured out the bulk of the console’s secrets.
|
||||
Look for debugging tools, but be ready to learn assembly.
|
||||
Old consoles usually have their own unique dialects of ASM you’ll need to get used to.
|
||||
|
||||
Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these
|
||||
older consoles to the Internet.
|
||||
There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a computer,
|
||||
but these will require the same sort of interface software to be written in order to work properly - from your perspective
|
||||
the two won't really look any different.
|
||||
|
||||
### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that-
|
||||
Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
|
||||
Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be
|
||||
working from scratch.
|
||||
|
||||
## How to Distribute Game Modifications
|
||||
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
|
||||
|
||||
This is a good way to get any project you're working on sued out from under you.
|
||||
The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you
|
||||
to copy them wholesale, is as patches.
|
||||
|
||||
There are many patch formats, which I'll cover in brief. The common theme is that you can’t distribute anything that wasn't
|
||||
made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding the
|
||||
issue of distributing someone else’s original work.
|
||||
|
||||
Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play.
|
||||
|
||||
### IPS Patches
|
||||
This is an extremely simple, early patch format, but is limited to games of about 16 Megabytes in size or less.
|
||||
You will often find IPS patches being used to distribute mods for old video game ROMs.
|
||||
IPS patches are a delta patch format, which means they act only as a simple list of alterations that need to be made to
|
||||
an original file in order to produce a new one.
|
||||
|
||||
Archipelago may use pre-made IPS patches to apply specific changes to a game, but will not create IPS patches as a means
|
||||
of distributing game modifications. Although IPS patches can be applied quickly, creating them is quite slow, so using
|
||||
them for distributing randomized games is not current practice.
|
||||
|
||||
However, due to the format's simplicity, even patch files of this type can unintentionally include copyrighted data.
|
||||
This is because IPS patches don't have a good way to shift existing data in a file, and thus if data has to be moved
|
||||
forward x number of bytes, which might be necessary for data insertion, the patch will simply include a copy of the
|
||||
shifted bytes after the inserted ones.
|
||||
Increasing and decreasing file size is also not a universally supported operation, due to the patch format's age.
|
||||
|
||||
### BPS Patches
|
||||
BPS is the younger cousin of the IPS patch.
|
||||
|
||||
More flexible and theoretically future-proofed for any file size, BPS patches are based on the idea of linear patching.
|
||||
Unlike IPS patches, which use a system called delta patching, linear patches act as a series of steps for creating a
|
||||
modified file from scratch through a combination of original data and patch data, which is appended onto the end of the
|
||||
modified game file as the patch progresses.
|
||||
|
||||
This means that some operations, like inserting data into the middle of a file instead of simply overwriting it,
|
||||
are much easier to do.
|
||||
However, like IPS, it isn't a format well suited to randomizers, due to the asymmetric costs of creating and applying
|
||||
BPS patches.
|
||||
|
||||
### Xdelta Patches
|
||||
Xdelta is the true successor to IPS, featuring better optimization and verification, and manages to transcend many of
|
||||
the limitations of IPS. However, Xdelta patches are particularly expensive to create.
|
||||
|
||||
### bsdiff
|
||||
bsdiff is the current format adopted by Archipelago for creating and distributing patches.
|
||||
It is much faster to create patches of this variety, which is why it sees use in this application.
|
||||
|
||||
### Mod files
|
||||
Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere.
|
||||
Mod files come in many forms, but the rules about not distributing other people's content remain the same.
|
||||
|
||||
## Archipelago Integration
|
||||
Integrating a randomizer into Archipelago involves a few steps.
|
||||
There are several things that may need to be done, but the most important is to create an implementation of the
|
||||
`World` class specific to your game. This implementation should exist as a Python module within the `worlds` folder
|
||||
in the Archipelago file structure.
|
||||
|
||||
This encompasses most of the data for your game – the items available, what checks you have, the logic for reaching those
|
||||
checks, what options to offer for the player’s yaml file, and the code to initialize all this data.
|
||||
|
||||
Here’s an example of what your world module can look like:
|
||||
|
||||

|
||||
|
||||
Let's give a quick breakdown of what the contents for these files look like.
|
||||
This is just one example of an Archipelago world - the way things are done below is not an immutable property of Archipelago.
|
||||
|
||||
### Items.py
|
||||
This file is used to define the items which exist in a given game.
|
||||
|
||||

|
||||
|
||||
Some important things to note here. The center of our Items.py file is the item_table, which individually lists every
|
||||
item in the game and associates them with an ItemData.
|
||||
|
||||
This file is rather skeletal - most of the actual data has been stripped out for simplicity.
|
||||
Each ItemData gives a numeric ID to associate with the item and a boolean telling us whether the item might allow the
|
||||
player to do more than they would have been able to before.
|
||||
|
||||
Next there's the item_frequencies. This simply tells Archipelago how many times each item appears in the pool.
|
||||
Items that appear exactly once need not be listed - Archipelago will interpret absence from this dictionary as meaning
|
||||
that the item appears once.
|
||||
|
||||
Lastly, note the `lookup_id_to_name` dictionary, which is typically imported and used in your Archipelago `World`
|
||||
implementation. This is how Archipelago is told about the items in your world.
|
||||
|
||||
### Locations.py
|
||||
This file lists all locations in the game.
|
||||
|
||||

|
||||
|
||||
First is the achievement_table. It lists each location, the region that it can be found in (more on regions later),
|
||||
and a numeric ID to associate with each location.
|
||||
|
||||
The exclusion table is a series of dictionaries which are used to exclude certain checks from the pool of progression
|
||||
locations based on user settings, and the events table associates certain specific checks with specific items.
|
||||
|
||||
`lookup_id_to_name` is also present for locations, though this is a separate dictionary, to be clear.
|
||||
|
||||
### Options.py
|
||||
This file details options to be searched for in a player's YAML settings file.
|
||||
|
||||

|
||||
|
||||
There are several types of option Archipelago has support for.
|
||||
In our case, we have three separate choices a player can toggle, either On or Off.
|
||||
You can also have players choose between a number of predefined values, or have them provide a numeric value within a
|
||||
specified range.
|
||||
|
||||
### Regions.py
|
||||
This file contains data which defines the world's topology.
|
||||
In other words, it details how different regions of the game connect to each other.
|
||||
|
||||

|
||||
|
||||
`terraria_regions` contains a list of tuples.
|
||||
The first element of the tuple is the name of the region, and the second is a list of connections that lead out of the region.
|
||||
|
||||
`mandatory_connections` describe where the connection leads.
|
||||
|
||||
Above this data is a function called `link_terraria_structures` which uses our defined regions and connections to create
|
||||
something more usable for Archipelago, but this has been left out for clarity.
|
||||
|
||||
### Rules.py
|
||||
This is the file that details rules for what players can and cannot logically be required to do, based on items and settings.
|
||||
|
||||

|
||||
|
||||
This is the most complicated part of the job, and is one part of Archipelago that is likely to see some changes in the future.
|
||||
The first class, called `TerrariaLogic`, is an extension of the `LogicMixin` class.
|
||||
This is where you would want to define methods for evaluating certain conditions, which would then return a boolean to
|
||||
indicate whether conditions have been met. Your rule definitions should start with some sort of identifier to delineate it
|
||||
from other games, as all rules are mixed together due to `LogicMixin`. In our case, `_terraria_rule` would be a better name.
|
||||
|
||||
The method below, `set_rules()`, is where you would assign these functions as "rules", using lambdas to associate these
|
||||
functions or combinations of them (or any other code that evaluates to a boolean, in my case just the placeholder `True`)
|
||||
to certain tasks, like checking locations or using entrances.
|
||||
|
||||
### \_\_init\_\_.py
|
||||
This is the file that actually extends the `World` class, and is where you expose functionality and data to Archipelago.
|
||||
|
||||

|
||||
|
||||
This is the most important file for the implementation, and technically the only one you need, but it's best to keep this
|
||||
file as short as possible and use other script files to do most of the heavy lifting.
|
||||
If you've done things well, this will just be where you assign everything you set up in the other files to their associated
|
||||
fields in the class being extended.
|
||||
|
||||
This is also a good place to put game-specific quirky behavior that needs to be managed, as it tends to make things a bit
|
||||
cluttered if you put these things elsewhere.
|
||||
|
||||
The various methods and attributes are documented in `/worlds/AutoWorld.py[World]`,
|
||||
though it is also recommended to look at existing implementations to see how all this works first-hand.
|
||||
Once you get all that, all that remains to do is test the game and publish your work.
|
||||
BIN
docs/img/archipelago-world-directory-example.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
docs/img/creepy-castle-directory.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/img/example-init-py-file.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
docs/img/example-items-py-file.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/img/example-locations-py-file.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/img/example-options-py-file.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
docs/img/example-regions-py-file.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/img/example-rules-py-file.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
docs/img/gato-roboto-directory.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/img/heavy-bullets-data-directory.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/img/heavy-bullets-directory.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/img/heavy-bullets-managed-directory.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/img/stardew-valley-directory.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
@@ -83,7 +83,13 @@ Sent to clients when the server refuses connection. This is sent during the init
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `SlotAlreadyTaken`, `IncompatibleVersion`, or `InvalidPassword`. |
|
||||
| errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `InvalidGame`, `SlotAlreadyTaken`, `IncompatibleVersion`, or `InvalidPassword`. |
|
||||
|
||||
InvalidSlot indicates that the sent 'name' field did not match any auth entry on the server.
|
||||
InvalidGame indicates that a correctly named slot was found, but the game for it mismatched.
|
||||
SlotAlreadyTaken indicates a connection with a different uuid is already established.
|
||||
IncompatibleVersion indicates a version mismatch.
|
||||
InvalidPassword indicates the wrong, or no password when it was required, was sent.
|
||||
|
||||
### Connected
|
||||
Sent to clients when the connection handshake is successfully completed.
|
||||
@@ -229,7 +235,7 @@ Requests the data package from the server. Does not require client authenticatio
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ------ | ----- | ------ |
|
||||
| exlusions | list[str] | Optional. If specified, will not send back the specified data. Such as, ["Factorio"] -> Datapackage without Factorio data.|
|
||||
| exclusions | list[str] | Optional. If specified, will not send back the specified data. Such as, ["Factorio"] -> Datapackage without Factorio data.|
|
||||
|
||||
### Bounce
|
||||
Send this message to the server, tell it which clients should receive the message and
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="371.93147284375027" width="260.192" x="928.2661119999984" y="362.8610391562502"/>
|
||||
<y:Geometry height="371.93147284375027" width="495.5861119999986" x="1227.445695999997" y="362.8610391562502"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="260.192" x="0.0" xml:space="preserve" y="0.0">Factorio</y:NodeLabel>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="495.5861119999986" x="0.0" xml:space="preserve" y="0.0">Factorio</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="16" rightF="15.871999999999844" top="0" topF="0.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
@@ -57,10 +57,10 @@
|
||||
<node id="n1::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="943.2661119999985" y="400.2375040000002"/>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="1242.445695999997" y="400.2375040000002"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.02734375" x="68.14632812499997" xml:space="preserve" y="15.889414062500009">FactorioClient<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.02734375" x="68.14632812499985" xml:space="preserve" y="15.889414062500009">FactorioClient<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -68,7 +68,7 @@
|
||||
<node id="n1::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="943.2661119999984" y="550.637504"/>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="1242.445695999997" y="550.637504"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.025390625" x="64.14730468749985" xml:space="preserve" y="15.889414062500009">Factorio Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
@@ -79,7 +79,7 @@
|
||||
<node id="n1::n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="943.2661119999984" y="669.3125120000004"/>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="1242.445695999997" y="669.3125120000004"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="89.359375" x="62.480312499999854" xml:space="preserve" y="15.889414062500009">Factorio Games<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
@@ -87,6 +87,17 @@
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n1::n3">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="1493.7118079999957" y="609.9750080000002"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="153.408203125" x="30.455898437500082" xml:space="preserve" y="15.889414062500009">Generated AP Factorio Mod<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n2" yfiles.foldertype="group">
|
||||
@@ -94,16 +105,276 @@
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GenericGroupNode configuration="PanelNode">
|
||||
<y:Geometry height="258.96281049999993" width="574.4661119999987" x="593.5730559999993" y="76.87344550000034"/>
|
||||
<y:Fill color="#68B0E3" transparent="false"/>
|
||||
<y:BorderStyle hasColor="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" borderDistance="0.0" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.6015625" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#FFFFFF" verticalTextPosition="bottom" visible="true" width="574.4661119999987" x="0.0" xml:space="preserve" y="0.0">WebHost (archipelago.gg)</y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.awt.Color" name="headerBackground" value="#68b0e3"/>
|
||||
</y:StyleProperties>
|
||||
<y:State autoResize="true" closed="false" closedHeight="50.0" closedWidth="50.0"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="24" topF="24.0"/>
|
||||
</y:GenericGroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="327.72897684375016" width="244.31999999999994" x="593.5730559999993" y="513.26103915625"/>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="244.31999999999994" x="0.0" xml:space="preserve" y="0.0">A Link to the Past</y:NodeLabel>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.02685546875" x="-4.513427734375" xml:space="preserve" y="0.0">Folder 3</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n2:">
|
||||
<node id="n2::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="139.47500800000034"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.34765625" x="80.48617187499997" xml:space="preserve" y="15.889414062500009">WebHost<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="938.719167999998" y="139.47500800000034"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="102.70703125" x="55.80648437500008" xml:space="preserve" y="15.889414062500009">Flask WebContent<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="270.35625600000026"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="64.029296875" x="75.14535156249997" xml:space="preserve" y="15.889414062500009">AutoHoster<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n3">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="938.7191679999981" y="269.85625600000026"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="79.3515625" x="67.48421874999997" xml:space="preserve" y="15.889414062500009">PonyORM DB<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n3" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="330.7612479999998" width="244.31999999999994" x="-239.98860800000648" y="247.97979115625026"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="244.31999999999994" x="0.0" xml:space="preserve" y="0.0">Unity/.Net</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.02685546875" x="-4.513427734375" xml:space="preserve" y="0.0">Folder 5</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n3:">
|
||||
<node id="n3::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="-224.98860800000648" y="400.2375040000002"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="200.083984375" x="7.118007812499968" xml:space="preserve" y="15.889414062500009">Mod with Archipelago.MultiClient.Net<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n3::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="-224.98860800000648" y="513.26103915625"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="64.046875" x="75.13656249999997" xml:space="preserve" y="15.889414062500009">Subnautica<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n3::n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="-224.98860800000648" y="285.35625600000026"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.6953125" x="67.81234374999997" xml:space="preserve" y="15.889414062500009">Risk of Rain 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n4" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="394.1644025312502" width="532.0391679999973" x="42.86527999999544" y="501.14561346875007"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="532.0391679999973" x="0.0" xml:space="preserve" y="0.0">Java</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="2" bottomF="1.8610391562500581" left="0" leftF="0.0" right="5" rightF="4.865279999995437" top="12" topF="12.115425687499965"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.02685546875" x="-4.513427734375" xml:space="preserve" y="0.0">Folder 6</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n4:">
|
||||
<node id="n4::n0" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="211.29396884375012" width="244.31999999999994" x="310.71916799999735" y="667.1550080000001"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="244.31999999999994" x="0.0" xml:space="preserve" y="0.0">Minecraft</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.02685546875" x="-4.513427734375" xml:space="preserve" y="0.0">Folder 4</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n4::n0:">
|
||||
<node id="n4::n0::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="325.71916799999735" y="704.5314728437501"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="126.70703125" x="43.80648437499997" xml:space="preserve" y="15.889414062500009">Minecraft Forge Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n4::n0::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="325.7191679999974" y="812.9689768437502"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="146.0546875" x="34.13265624999997" xml:space="preserve" y="15.889414062500009">Any Java Minecraft Clients<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n4::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="325.71916799999735" y="550.637504"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="206.7578125" x="3.781093749999968" xml:space="preserve" y="15.889414062500009">Mod with Archipelago.MultiClient.Java<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n4::n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="57.86527999999544" y="550.637504"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.70703125" x="67.80648437499997" xml:space="preserve" y="15.889414062500009">Slay the Spire<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n5">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="938.719167999998" y="400.2375040000002"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="99.3671875" x="57.47640625000008" xml:space="preserve" y="15.889414062500009">CommonClient.py<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n6" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="335.8914728437501" width="603.8726399999978" x="593.5730559999993" y="513.26103915625"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="603.8726399999978" x="0.0" xml:space="preserve" y="0.0">A Link to the Past</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="29" rightF="29.406527999998843" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
@@ -117,22 +388,22 @@
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n2:">
|
||||
<node id="n2::n0">
|
||||
<graph edgedefault="directed" id="n6:">
|
||||
<node id="n6::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="550.637504"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="104.04296875" x="55.13851562499997" xml:space="preserve" y="15.889414062500009">LttPClient/Z3Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="48.68359375" x="82.81820312499997" xml:space="preserve" y="15.889414062500009">Z3Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n1">
|
||||
<node id="n6::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="667.1550080000001"/>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="773.6461119999987" y="667.1550080000001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="24.00390625" x="95.15804687499997" xml:space="preserve" y="15.889414062500009">SNI<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
@@ -140,10 +411,10 @@
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2::n2">
|
||||
<node id="n6::n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="775.5100160000002"/>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="773.6461119999987" y="783.6725120000001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="36.677734375" x="88.82113281249997" xml:space="preserve" y="15.889414062500009">SNES<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
@@ -151,148 +422,85 @@
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n6::n3">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="938.7191679999983" y="550.637504"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="56.025390625" x="79.14730468749985" xml:space="preserve" y="15.889414062500009">LttPClient<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n3" yfiles.foldertype="group">
|
||||
<node id="n7" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="264.1377128437499" width="594.8850559999992" x="593.5730559999993" y="71.69854315625034"/>
|
||||
<y:Geometry height="297.73771284374993" width="224.31999999999994" x="320.7305599999909" y="28.098543156250358"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="594.8850559999992" x="0.0" xml:space="preserve" y="0.0">WebHost (archipelago.gg)</y:NodeLabel>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="224.31999999999994" x="0.0" xml:space="preserve" y="0.0">Ocarina of Time</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="20" rightF="20.418944000000465" top="30" topF="30.400000000000006"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Geometry height="358.96281049999993" width="581.2" x="-10.668608000006543" y="14.925000000000239"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.02685546875" x="-4.513427734375" xml:space="preserve" y="0.0">Folder 3</y:NodeLabel>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="581.2" x="0.0" xml:space="preserve" y="0.0">1</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:State closed="true" closedHeight="358.96281049999993" closedWidth="581.2" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n3:">
|
||||
<node id="n3::n0">
|
||||
<graph edgedefault="directed" id="n7:">
|
||||
<node id="n7::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="139.47500800000034"/>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="325.7305599999909" y="270.35625600000026"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.34765625" x="80.48617187499997" xml:space="preserve" y="15.889414062500009">WebHost<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="48.68359375" x="82.81820312499997" xml:space="preserve" y="15.889414062500009">Z5Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n3::n1">
|
||||
<node id="n7::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="938.719167999998" y="139.47500800000034"/>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="325.7305599999909" y="162.9156320000003"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="102.70703125" x="55.80648437500008" xml:space="preserve" y="15.889414062500009">Flask WebContent<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="82.720703125" x="65.79964843749997" xml:space="preserve" y="15.889414062500009">Lua Connector<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n3::n2">
|
||||
<node id="n7::n2">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="270.35625600000026"/>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="325.7305599999909" y="55.47500800000036"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="64.029296875" x="75.14535156249997" xml:space="preserve" y="15.889414062500009">AutoHoster<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n3::n3">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="938.7191679999981" y="269.85625600000026"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="79.3515625" x="67.48421874999997" xml:space="preserve" y="15.889414062500009">PonyORM DB<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="203.412109375" x="5.453945312499968" xml:space="preserve" y="15.889414062500009">BizHawk with Ocarina of Time loaded<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n4" yfiles.foldertype="group">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d6">
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="211.29396884375012" width="244.31999999999994" x="298.1461119999983" y="513.26103915625"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="244.31999999999994" x="0.0" xml:space="preserve" y="0.0">Minecraft</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.02685546875" x="-4.513427734375" xml:space="preserve" y="0.0">Folder 4</y:NodeLabel>
|
||||
<y:Shape type="roundrectangle"/>
|
||||
<y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
|
||||
<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
|
||||
<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
|
||||
</y:GroupNode>
|
||||
</y:Realizers>
|
||||
</y:ProxyAutoBoundsNode>
|
||||
</data>
|
||||
<graph edgedefault="directed" id="n4:">
|
||||
<node id="n4::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="313.1461119999983" y="550.637504"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="173.40625" x="20.456874999999968" xml:space="preserve" y="15.889414062500009">Modded Minecraft Forge Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n4::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="313.14611199999837" y="659.0750080000001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="117.373046875" x="48.47347656249997" xml:space="preserve" y="15.889414062500009">Any Minecraft Clients<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n5">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="313.1461119999983" y="270.35625600000026"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="110.74609375" x="51.78695312499997" xml:space="preserve" y="15.889414062500009">Modded Subnautica<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<edge id="e0" source="n2::n0" target="n0">
|
||||
<edge id="e0" source="n6::n0" target="n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="155.21347200000048" ty="-25.23531650000018">
|
||||
@@ -304,7 +512,7 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e1" source="n0" target="n2::n0">
|
||||
<edge id="e1" source="n0" target="n6::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="155.21347200000048" sy="-7.977504000000181" tx="0.0" ty="0.0"/>
|
||||
@@ -315,7 +523,7 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n2::e0" source="n2::n0" target="n2::n1">
|
||||
<edge id="n6::e0" source="n6::n0" target="n6::n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
@@ -325,18 +533,18 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n2::e1" source="n2::n1" target="n2::n0">
|
||||
<edge id="n6::e1" source="n6::n1" target="n6::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="17.800336273436756" xml:space="preserve" y="-42.369359234375">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="50.48000000000002" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="36.009765625" x="-24.094081989207098" xml:space="preserve" y="-50.369344625435815">gRPC<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n2::e2" source="n2::n1" target="n2::n2">
|
||||
<edge id="n6::e2" source="n6::n1" target="n6::n2">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
@@ -346,34 +554,13 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n2::e3" source="n2::n2" target="n2::n1">
|
||||
<edge id="n6::e3" source="n6::n2" target="n6::n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="214.12890625" x="-107.06442935156315" xml:space="preserve" y="-38.28808370312481">Various, depends on SNES device type<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="center" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e2" source="n0" target="n1::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e3" source="n1::n0" target="n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="175.50327055767139" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="-92.86621678125107" xml:space="preserve" y="-39.350590482421694">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="214.12890625" x="-107.06443243359513" xml:space="preserve" y="-42.36931128906235">Various, depends on SNES device type<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="center" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@@ -382,11 +569,11 @@
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
|
||||
<y:Point x="1050.4261119999983" y="500.5"/>
|
||||
<y:Point x="1349.605695999997" y="500.5"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="39.33203125" x="-48.064825261609485" xml:space="preserve" y="40.06887780599749">RCON<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="8.732758550670264" distanceToCenter="false" position="right" ratio="-0.36303747720564117" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="39.33203125" x="-48.06480669129837" xml:space="preserve" y="40.06887780599749">RCON<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="8.732758550670264" distanceToCenter="false" position="right" ratio="-0.36303747720564117" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@@ -407,12 +594,12 @@
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="29.3359375" x="15.331995789060784" xml:space="preserve" y="-43.44807793749976">UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="29.3359375" x="15.332014359371897" xml:space="preserve" y="-43.44807793749976">UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n3::e0" source="n3::n0" target="n3::n1">
|
||||
<edge id="n2::e0" source="n2::n0" target="n2::n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
@@ -423,7 +610,7 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n3::e1" source="n3::n0" target="n3::n2">
|
||||
<edge id="n2::e1" source="n2::n0" target="n2::n2">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
@@ -434,7 +621,7 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e4" source="n3::n2" target="n0">
|
||||
<edge id="e2" source="n2::n2" target="n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="155.21347200000048" ty="-3.977504000000181"/>
|
||||
@@ -445,7 +632,7 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n3::e2" source="n3::n3" target="n3::n1">
|
||||
<edge id="n2::e2" source="n2::n3" target="n2::n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
@@ -456,7 +643,7 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n3::e3" source="n3::n1" target="n3::n3">
|
||||
<edge id="n2::e3" source="n2::n1" target="n2::n3">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
@@ -466,7 +653,7 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n3::e4" source="n3::n3" target="n3::n2">
|
||||
<edge id="n2::e4" source="n2::n3" target="n2::n2">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
@@ -477,7 +664,7 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n3::e5" source="n3::n2" target="n3::n3">
|
||||
<edge id="n2::e5" source="n2::n2" target="n2::n3">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
@@ -487,28 +674,7 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e5" source="n0" target="n4::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-140.21347200000048" sy="-1.977504000000181" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e6" source="n4::n0" target="n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="-140.21347200000048" ty="-12.977504000000181"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="32.320302673826404" xml:space="preserve" y="-78.31059414453114">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="65.0" distanceToCenter="true" position="right" ratio="0.7667833843973411" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n4::e0" source="n4::n0" target="n4::n1">
|
||||
<edge id="n4::n0::e0" source="n4::n0::n0" target="n4::n0::n1">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
@@ -518,36 +684,203 @@
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n4::e1" source="n4::n1" target="n4::n0">
|
||||
<edge id="n4::n0::e1" source="n4::n0::n1" target="n4::n0::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="28.0" x="15.999990173826461" xml:space="preserve" y="-38.32934214453121">TCP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="28.0" x="15.999987091794196" xml:space="preserve" y="-38.329355234374816">TCP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e7" source="n5" target="n0">
|
||||
<edge id="e3" source="n3::n0" target="n0">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="-138.06736000000217" ty="25.21780849999982"/>
|
||||
<y:Path sx="0.0" sy="0.0" tx="-140.21347200000048" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="33.223286918485144" xml:space="preserve" y="31.70008944921858">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="65.32870706273742" distanceToCenter="true" position="left" ratio="0.5266279296932641" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="121.72767221178788" xml:space="preserve" y="20.64940951757825">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e8" source="n0" target="n5">
|
||||
<edge id="n3::e0" source="n3::n1" target="n3::n0">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-140.21347200000048" sy="4.022495999999819" tx="0.0" ty="0.0"/>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="74.04296875" x="7.9785132768489575" xml:space="preserve" y="-41.62236172265614">QModLoader<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="45.0" distanceToCenter="true" position="right" ratio="0.5295487638286196" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n3::e1" source="n3::n2" target="n3::n0">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="49.36328125" x="5.3183570268489575" xml:space="preserve" y="22.850051386718974">BepInEx<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="left" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n4::e0" source="n4::n2" target="n4::n1">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.70703125" x="-11.586563841800512" xml:space="preserve" y="20.649415621093794">Mod the Spire<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="1.0" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n4::e1" source="n4::n0::n0" target="n4::n1">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="102.724609375" x="16.637682404294083" xml:space="preserve" y="-76.05759165625">Forge Mod Loader<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="67.99999999999994" distanceToCenter="true" position="right" ratio="0.7007688188447031" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e4" source="n4::n1" target="n0">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="-127.64041600000144" ty="-2.977504000000181"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="34.320299591794196" xml:space="preserve" y="-74.31059414453114">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="67.0" distanceToCenter="true" position="right" ratio="0.7106184613663219" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n1::e3" source="n1::n3" target="n1::n2">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
|
||||
<y:Point x="1600.8718079999958" y="694.5525120000004"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n1::e4" source="n1::n3" target="n1::n1">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
|
||||
<y:Point x="1600.8718079999958" y="575.877504"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e5" source="n0" target="n5">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="25.23335809374862" xml:space="preserve" y="20.649409517578306">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n6::e4" source="n6::n3" target="n6::n1">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="-120.1473927412643" xml:space="preserve" y="15.668191995657935">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e6" source="n6::n3" target="n5">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="58.041015625" x="0.979509796873117" xml:space="preserve" y="-74.61573791505123">Integrated<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.7149030554624851" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e7" source="n1::n0" target="n5">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="58.041015625" x="-73.72375452344" xml:space="preserve" y="-39.350590482421694">Integrated<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e8" source="n7::n0" target="n0">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="-127.62902400000792" ty="5.022495999999819"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="33.32030853514709" xml:space="preserve" y="30.350051386718974">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="66.0" distanceToCenter="true" position="left" ratio="0.5" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n7::e0" source="n7::n1" target="n7::n0">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="66.70703125" x="30.646480410147092" xml:space="preserve" y="18.12972817968779">LuaSockets<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="64.0" distanceToCenter="true" position="left" ratio="0.46461360979056837" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="n7::e1" source="n7::n2" target="n7::n1">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="standard" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="-32.00000396485291" y="26.480310539551112">
|
||||
<y:LabelModel>
|
||||
<y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/>
|
||||
</y:LabelModel>
|
||||
<y:ModelParameter>
|
||||
<y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/>
|
||||
</y:ModelParameter>
|
||||
<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
|
||||
</y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
|
||||
BIN
docs/network.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
@@ -50,6 +50,8 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom
|
||||
[Components]
|
||||
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
||||
Name: "generator"; Description: "Generator"; Types: full hosting
|
||||
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup"; Types: full hosting
|
||||
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting
|
||||
Name: "server"; Description: "Server"; Types: full hosting
|
||||
Name: "client"; Description: "Clients"; Types: full playing
|
||||
Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing hosting
|
||||
@@ -60,7 +62,8 @@ Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDi
|
||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||
|
||||
[Files]
|
||||
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/lttp or generator
|
||||
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/lttp or generator/lttp
|
||||
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: generator/oot
|
||||
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
|
||||
@@ -186,6 +189,8 @@ end;
|
||||
var ROMFilePage: TInputFileWizardPage;
|
||||
var R : longint;
|
||||
var rom: string;
|
||||
var ootrom: string;
|
||||
var OoTROMFilePage: TInputFileWizardPage;
|
||||
var MinecraftDownloadPage: TDownloadWizardPage;
|
||||
|
||||
procedure AddRomPage();
|
||||
@@ -221,6 +226,36 @@ begin
|
||||
MinecraftDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadMinecraftProgress);
|
||||
end;
|
||||
|
||||
procedure AddOoTRomPage();
|
||||
begin
|
||||
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
|
||||
if Length(ootrom) > 0 then
|
||||
begin
|
||||
log('existing ROM found');
|
||||
log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6'))); // normal
|
||||
log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b'))); // byteswapped
|
||||
log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f'))); // decompressed
|
||||
if (CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6') = 0) or (CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b') = 0) or (CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f') = 0) then
|
||||
begin
|
||||
log('existing ROM verified');
|
||||
exit;
|
||||
end;
|
||||
log('existing ROM failed verification');
|
||||
end;
|
||||
ootrom := ''
|
||||
OoTROMFilePage :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your OoT 1.0 ROM located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
OoTROMFilePage.Add(
|
||||
'Location of ROM file:',
|
||||
'N64 ROM files (*.z64, *.n64)|*.z64;*.n64|All files|*.*',
|
||||
'.z64');
|
||||
end;
|
||||
|
||||
function NextButtonClick(CurPageID: Integer): Boolean;
|
||||
begin
|
||||
if (CurPageID = wpReady) and (WizardIsComponentSelected('client/minecraft')) then begin
|
||||
@@ -253,7 +288,8 @@ begin
|
||||
end;
|
||||
|
||||
procedure InitializeWizard();
|
||||
begin
|
||||
begin
|
||||
AddOoTRomPage();
|
||||
AddRomPage();
|
||||
AddMinecraftDownloads();
|
||||
end;
|
||||
@@ -263,7 +299,9 @@ function ShouldSkipPage(PageID: Integer): Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
if (assigned(ROMFilePage)) and (PageID = ROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('client/lttp') or WizardIsComponentSelected('generator'));
|
||||
Result := not (WizardIsComponentSelected('client/lttp') or WizardIsComponentSelected('generator/lttp'));
|
||||
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/oot'));
|
||||
end;
|
||||
|
||||
function GetROMPath(Param: string): string;
|
||||
@@ -274,10 +312,26 @@ begin
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(ROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
|
||||
if R <> 0 then
|
||||
MsgBox('ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := ROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetOoTROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(ootrom) > 0 then
|
||||
Result := ootrom
|
||||
else if Assigned(OoTROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f');
|
||||
if R <> 0 then
|
||||
MsgBox('OoT ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := OoTROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
1
kvui.py
@@ -150,4 +150,5 @@ class KivyJSONtoTextParser(JSONtoTextParser):
|
||||
ExceptionManager.add_handler(E())
|
||||
|
||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||
Config.set('kivy', 'exit_on_escape', '0')
|
||||
Builder.load_file(Utils.local_path("data", "client.kv"))
|
||||
|
||||
@@ -29,8 +29,9 @@ game: # Pick a game to play
|
||||
Minecraft: 0
|
||||
Subnautica: 0
|
||||
Slay the Spire: 0
|
||||
Ocarina of Time: 0
|
||||
requires:
|
||||
version: 0.1.6 # Version of Archipelago required for this yaml to work as expected.
|
||||
version: 0.1.7 # Version of Archipelago required for this yaml to work as expected.
|
||||
# Shared Options supported by all games:
|
||||
accessibility:
|
||||
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
|
||||
@@ -722,13 +723,20 @@ Ocarina of Time:
|
||||
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.
|
||||
triforce_goal: # Number of Triforce pieces required to complete the game.
|
||||
# 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
|
||||
extra_triforce_percentage: # Percentage of additional Triforce pieces in the pool, separate from the item pool setting.
|
||||
# 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
|
||||
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
|
||||
@@ -836,14 +844,17 @@ Ocarina of Time:
|
||||
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.
|
||||
shopsanity: # Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop; "random_number" randomizes the value for each shop.
|
||||
off: 50
|
||||
"0": 0
|
||||
"1": 0
|
||||
"2": 0
|
||||
"3": 0
|
||||
"4": 0
|
||||
random_value: 0
|
||||
fixed_number: 0
|
||||
random_number: 0
|
||||
shop_slots: # Number of items per shop to be randomized into the main itempool. Only active if Shopsanity is set to "fixed_number."
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
4: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool.
|
||||
off: 50
|
||||
dungeons: 0
|
||||
@@ -912,6 +923,9 @@ Ocarina of Time:
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
correct_chest_sizes: # Changes chests containing progression into large chests, and nonprogression into small chests.
|
||||
false: 50
|
||||
true: 0
|
||||
hints: # Gossip Stones can give hints about item locations.
|
||||
none: 0
|
||||
mask: 0
|
||||
@@ -1283,8 +1297,26 @@ Ocarina of Time:
|
||||
harp: 0
|
||||
grind_organ: 0
|
||||
flute: 0
|
||||
logic_tricks:
|
||||
[]
|
||||
|
||||
# Uncomment this section to enable logical tricks for Ocarina of Time.
|
||||
# Add logic tricks keyed by "nice" name rather than internal name: "Hidden Grottos without Stone of Agony", not "logic_grottos_without_agony"
|
||||
# The following is the typical set of racing tricks, though you can add or remove them as desired.
|
||||
# logic_tricks:
|
||||
# - Fewer Tunic Requirements
|
||||
# - Hidden Grottos without Stone of Agony
|
||||
# - Child Deadhand without Kokiri Sword
|
||||
# - Man on Roof without Hookshot
|
||||
# - Dodongo's Cavern Spike Trap Room Jump without Hover Boots
|
||||
# - Hammer Rusted Switches Through Walls
|
||||
# - Windmill PoH as Adult with Nothing
|
||||
# - Crater's Bean PoH with Hover Boots
|
||||
# - Forest Temple East Courtyard Vines with Hookshot
|
||||
# - Bottom of the Well without Lens of Truth
|
||||
# - Ganon's Castle without Lens of Truth
|
||||
# - Gerudo Training Grounds without Lens of Truth
|
||||
# - Shadow Temple before Invisible Moving Platform without Lens of Truth
|
||||
# - Shadow Temple beyond Invisible Moving Platform without Lens of Truth
|
||||
# - Spirit Temple without Lens of Truth
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
colorama>=0.4.4
|
||||
websockets>=9.1
|
||||
websockets>=10.0
|
||||
PyYAML>=5.4.1
|
||||
fuzzywuzzy>=0.18.0
|
||||
prompt_toolkit>=3.0.20
|
||||
|
||||
@@ -19,6 +19,7 @@ class TestDungeon(unittest.TestCase):
|
||||
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.set_default_common_options()
|
||||
self.starting_regions = [] # Where to start exploring
|
||||
self.remove_exits = [] # Block dungeon exits
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
|
||||
@@ -19,6 +19,7 @@ class TestInverted(TestBase):
|
||||
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.set_default_common_options()
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
self.world.mode[1] = "inverted"
|
||||
create_inverted_regions(self.world, 1)
|
||||
|
||||
@@ -20,6 +20,7 @@ class TestInvertedBombRules(unittest.TestCase):
|
||||
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.set_default_common_options()
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
create_inverted_regions(self.world, 1)
|
||||
create_dungeons(self.world, 1)
|
||||
|
||||
@@ -20,6 +20,7 @@ class TestInvertedMinor(TestBase):
|
||||
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.set_default_common_options()
|
||||
self.world.mode[1] = "inverted"
|
||||
self.world.logic[1] = "minorglitches"
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
|
||||
@@ -21,6 +21,7 @@ class TestInvertedOWG(TestBase):
|
||||
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.set_default_common_options()
|
||||
self.world.logic[1] = "owglitches"
|
||||
self.world.mode[1] = "inverted"
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
|
||||
@@ -20,6 +20,7 @@ class TestMinor(TestBase):
|
||||
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.set_default_common_options()
|
||||
self.world.logic[1] = "minorglitches"
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
create_regions(self.world, 1)
|
||||
|
||||
@@ -21,6 +21,7 @@ class TestVanillaOWG(TestBase):
|
||||
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.set_default_common_options()
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
self.world.logic[1] = "owglitches"
|
||||
create_regions(self.world, 1)
|
||||
|
||||
@@ -19,6 +19,7 @@ class TestVanilla(TestBase):
|
||||
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.set_default_common_options()
|
||||
self.world.logic[1] = "noglitches"
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
create_regions(self.world, 1)
|
||||
|
||||
@@ -91,6 +91,10 @@ class World(metaclass=AutoWorldRegister):
|
||||
# the client finds its own items in its own world.
|
||||
remote_items: bool = True
|
||||
|
||||
# If remote_start_inventory is true, the start_inventory/world.precollected_items is sent on connection,
|
||||
# otherwise the world implementation is in charge of writing the items to their output data.
|
||||
remote_start_inventory: 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
|
||||
@@ -176,7 +180,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
Warning: this may be called with self.world = None, for example by MultiServer"""
|
||||
raise NotImplementedError
|
||||
|
||||
# following methods should not need to be overriden.
|
||||
# following methods should not need to be overridden.
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
name = self.collect_item(state, item)
|
||||
if name:
|
||||
|
||||
@@ -20,7 +20,6 @@ def parse_arguments(argv, no_defaults=False):
|
||||
multiargs, _ = parser.parse_known_args(argv)
|
||||
|
||||
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true')
|
||||
parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'owglitches', 'hybridglitches', 'nologic'],
|
||||
help='''\
|
||||
Select Enforcement of Item Requirements. (default: %(default)s)
|
||||
@@ -46,17 +45,6 @@ def parse_arguments(argv, no_defaults=False):
|
||||
Requires the moon pearl to be Link in the Light World
|
||||
instead of a bunny.
|
||||
''')
|
||||
parser.add_argument('--swordless', action='store_true',
|
||||
help='''\
|
||||
Toggles Swordless Mode
|
||||
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s
|
||||
Tower are removed, Agahnim\'s Tower barrier can be
|
||||
destroyed with hammer. Misery Mire and Turtle Rock
|
||||
can be opened without a sword. Hammer damages Ganon.
|
||||
Ether and Bombos Tablet can be activated with Hammer
|
||||
(and Book). Bombos pads have been added in Ice
|
||||
Palace, to allow for an alternative to firerod.
|
||||
''')
|
||||
parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?',
|
||||
choices=['ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'],
|
||||
help='''\
|
||||
@@ -194,8 +182,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||
yes - Always opens the pyramid hole.
|
||||
no - Never opens the pyramid hole.
|
||||
''', choices=['auto', 'goal', 'yes', 'no'])
|
||||
parser.add_argument('--rom', default=defval('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc'),
|
||||
help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
|
||||
parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||
parser.add_argument('--count', help='''\
|
||||
@@ -206,26 +193,8 @@ def parse_arguments(argv, no_defaults=False):
|
||||
time).
|
||||
''', type=int)
|
||||
|
||||
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('--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(''),
|
||||
help='Specifies a list of items that will spread across the multiworld (separated by commas)')
|
||||
parser.add_argument('--custom', default=defval(False), help='Not supported.')
|
||||
parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
|
||||
parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
|
||||
Select Item/Location Accessibility. (default: %(default)s)
|
||||
Items: You can reach all unique inventory items. No guarantees about
|
||||
reaching all locations or all keys.
|
||||
Locations: You will be able to reach every location in the game.
|
||||
None: You will be able to reach enough locations to beat the game.
|
||||
''')
|
||||
parser.add_argument('--hints', default=defval(False), help='''\
|
||||
Make telepathic tiles and storytellers give helpful hints.
|
||||
''', action='store_true')
|
||||
# included for backwards compatibility
|
||||
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(True))
|
||||
parser.add_argument('--no-shuffleganon', help='''\
|
||||
@@ -241,20 +210,14 @@ def parse_arguments(argv, no_defaults=False):
|
||||
sprite that will be extracted.
|
||||
''')
|
||||
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
|
||||
parser.add_argument('--progression_balancing', action='store_true', default=defval(False),
|
||||
help="Enable Multiworld Progression balancing.")
|
||||
parser.add_argument('--skip_playthrough', action='store_true', default=defval(False))
|
||||
|
||||
parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
|
||||
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos',
|
||||
"singularity"])
|
||||
parser.add_argument('--enemy_shuffle', action='store_true')
|
||||
parser.add_argument('--killable_thieves', action='store_true')
|
||||
parser.add_argument('--tile_shuffle', action='store_true')
|
||||
parser.add_argument('--bush_shuffle', action='store_true')
|
||||
|
||||
parser.add_argument('--enemy_health', default=defval('default'),
|
||||
choices=['default', 'easy', 'normal', 'hard', 'expert'])
|
||||
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--shufflepots', default=defval(False), action='store_true')
|
||||
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
|
||||
parser.add_argument('--shop_shuffle', default='', help='''\
|
||||
combine letters for options:
|
||||
@@ -272,7 +235,6 @@ def parse_arguments(argv, no_defaults=False):
|
||||
For unlit dark rooms, require the Lamp to be considered in logic by default.
|
||||
Torches means additionally easily accessible Torches that can be lit with Fire Rod are considered doable.
|
||||
None means full traversal through dark rooms without tools is considered doable.''')
|
||||
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('--outputpath')
|
||||
@@ -307,19 +269,19 @@ def parse_arguments(argv, no_defaults=False):
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
|
||||
|
||||
for name in ['logic', 'mode', 'swordless', 'goal', 'difficulty', 'item_functionality',
|
||||
for name in ['logic', 'mode', 'goal', 'difficulty', 'item_functionality',
|
||||
'shuffle', 'open_pyramid', 'timer',
|
||||
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
|
||||
'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer',
|
||||
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||
'beemizer',
|
||||
'shufflebosses', 'enemy_health', 'enemy_damage',
|
||||
'sprite',
|
||||
"progression_balancing", "triforce_pieces_available",
|
||||
"triforce_pieces_available",
|
||||
"triforce_pieces_required", "shop_shuffle",
|
||||
"required_medallions", "start_hints",
|
||||
"plando_items", "plando_texts", "plando_connections", "er_seeds",
|
||||
'dungeon_counters', 'killable_thieves',
|
||||
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
|
||||
'restrict_dungeon_item_on_boss', 'game']:
|
||||
'dungeon_counters',
|
||||
'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
|
||||
'game']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
|
||||
@@ -244,7 +244,7 @@ def generate_itempool(world):
|
||||
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False)
|
||||
|
||||
if world.goal[player] == 'icerodhunt':
|
||||
world.progression_balancing[player] = False
|
||||
world.progression_balancing[player].value = False
|
||||
loc = world.get_location('Turtle Rock - Boss', player)
|
||||
world.push_item(loc, ItemFactory('Triforce Piece', player), False)
|
||||
world.treasure_hunt_count[player] = 1
|
||||
@@ -354,6 +354,12 @@ def generate_itempool(world):
|
||||
world.get_location(location, player).place_locked_item(ItemFactory(item, player))
|
||||
|
||||
items = ItemFactory(pool, player)
|
||||
# convert one Progressive Bow into Progressive Bow (Alt), in ID only, for ganon silvers hint text
|
||||
if world.worlds[player].has_progressive_bows:
|
||||
for item in items:
|
||||
if item.code == 0x64: # Progressive Bow
|
||||
item.code = 0x65 # Progressive Bow (Alt)
|
||||
break
|
||||
|
||||
if clock_mode is not None:
|
||||
world.clock_mode[player] = clock_mode
|
||||
@@ -584,6 +590,7 @@ def get_pool_core(world, player: int):
|
||||
|
||||
if want_progressives(world.random):
|
||||
pool.extend(diff.progressivebow)
|
||||
world.worlds[player].has_progressive_bows = True
|
||||
elif (swordless or logic == 'noglitches') and goal != 'icerodhunt':
|
||||
swordless_bows = ['Bow', 'Silver Bow']
|
||||
if difficulty == "easy":
|
||||
|
||||
@@ -126,6 +126,59 @@ class Progressive(Choice):
|
||||
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
|
||||
|
||||
|
||||
class Swordless(Toggle):
|
||||
"""No swords. Curtains in Skull Woods and Agahnim\'s
|
||||
Tower are removed, Agahnim\'s Tower barrier can be
|
||||
destroyed with hammer. Misery Mire and Turtle Rock
|
||||
can be opened without a sword. Hammer damages Ganon.
|
||||
Ether and Bombos Tablet can be activated with Hammer
|
||||
(and Book)."""
|
||||
displayname = "Swordless"
|
||||
|
||||
|
||||
class Retro(Toggle):
|
||||
"""Zelda-1 like mode. You have to purchase a quiver to shoot arrows using rupees
|
||||
and there are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion."""
|
||||
displayname = "Retro"
|
||||
|
||||
|
||||
class RestrictBossItem(Toggle):
|
||||
"""Don't place dungeon-native items on the dungeon's boss."""
|
||||
displayname = "Prevent Dungeon Item on Boss"
|
||||
|
||||
|
||||
class Hints(DefaultOnToggle):
|
||||
"""Put item and entrance placement hints on telepathic tiles and some NPCs.
|
||||
Additionally King Zora and Bottle Merchant say what they're selling."""
|
||||
displayname = "Hints"
|
||||
|
||||
|
||||
class EnemyShuffle(Toggle):
|
||||
"""Randomize every enemy spawn.
|
||||
If mode is Standard, Hyrule Castle is left out (may result in visually wrong enemy sprites in that area.)"""
|
||||
displayname = "Enemy Shuffle"
|
||||
|
||||
|
||||
class KillableThieves(Toggle):
|
||||
"""Makes Thieves killable."""
|
||||
displayname = "Killable Thieves"
|
||||
|
||||
|
||||
class BushShuffle(Toggle):
|
||||
"""Randomize chance that a bush contains an enemy as well as which enemy may spawn."""
|
||||
displayname = "Bush Shuffle"
|
||||
|
||||
|
||||
class TileShuffle(Toggle):
|
||||
"""Randomize flying tiles floor patterns."""
|
||||
displayname = "Tile Shuffle"
|
||||
|
||||
|
||||
class PotShuffle(Toggle):
|
||||
"""Shuffle contents of pots within "supertiles" (item will still be nearby original placement)."""
|
||||
displayname = "Pot Shuffle"
|
||||
|
||||
|
||||
class Palette(Choice):
|
||||
option_default = 0
|
||||
option_good = 1
|
||||
@@ -226,7 +279,16 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"compass_shuffle": compass_shuffle,
|
||||
"map_shuffle": map_shuffle,
|
||||
"progressive": Progressive,
|
||||
"swordless": Swordless,
|
||||
"retro": Retro,
|
||||
"hints": Hints,
|
||||
"restrict_dungeon_item_on_boss": RestrictBossItem,
|
||||
"pot_shuffle": PotShuffle,
|
||||
"enemy_shuffle": EnemyShuffle,
|
||||
"killable_thieves": KillableThieves,
|
||||
"bush_shuffle": BushShuffle,
|
||||
"shop_item_slots": ShopItemSlots,
|
||||
"tile_shuffle": TileShuffle,
|
||||
"ow_palettes": OWPalette,
|
||||
"uw_palettes": UWPalette,
|
||||
"hud_palettes": HUDPalette,
|
||||
|
||||
@@ -108,7 +108,7 @@ class LocalRom(object):
|
||||
self.encrypt_range(0x180140, 32, key)
|
||||
self.encrypt_range(0xEDA1, 8, key)
|
||||
|
||||
def write_to_file(self, file, hide_enemizer=False):
|
||||
def write_to_file(self, file):
|
||||
with open(file, 'wb') as outfile:
|
||||
outfile.write(self.buffer)
|
||||
|
||||
@@ -283,9 +283,9 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
|
||||
|
||||
# write options file for enemizer
|
||||
options = {
|
||||
'RandomizeEnemies': world.enemy_shuffle[player],
|
||||
'RandomizeEnemies': world.enemy_shuffle[player].value,
|
||||
'RandomizeEnemiesType': 3,
|
||||
'RandomizeBushEnemyChance': world.bush_shuffle[player],
|
||||
'RandomizeBushEnemyChance': world.bush_shuffle[player].value,
|
||||
'RandomizeEnemyHealthRange': world.enemy_health[player] != 'default',
|
||||
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
|
||||
world.enemy_health[player]],
|
||||
@@ -323,7 +323,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
|
||||
'GrayscaleMode': False,
|
||||
'GenerateSpoilers': False,
|
||||
'RandomizeLinkSpritePalette': False,
|
||||
'RandomizePots': world.shufflepots[player],
|
||||
'RandomizePots': world.pot_shuffle[player].value,
|
||||
'ShuffleMusic': False,
|
||||
'BootlegMagic': True,
|
||||
'CustomBosses': False,
|
||||
@@ -336,7 +336,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
|
||||
'BeesLevel': 0,
|
||||
'RandomizeTileTrapPattern': False,
|
||||
'RandomizeTileTrapFloorTile': False,
|
||||
'AllowKillableThief': world.killable_thieves[player],
|
||||
'AllowKillableThief': world.killable_thieves[player].value,
|
||||
'RandomizeSpriteOnHit': False,
|
||||
'DebugMode': False,
|
||||
'DebugForceEnemy': False,
|
||||
@@ -747,20 +747,13 @@ bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028,
|
||||
|
||||
|
||||
def get_nonnative_item_sprite(item: str) -> int:
|
||||
return 0x6B # set all non-native sprites to Power Star as per 13 to 2 vote at
|
||||
return 0x6B # set all non-native sprites to Power Star as per 13 to 2 vote at
|
||||
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
|
||||
|
||||
|
||||
def patch_rom(world, rom, player, enemized):
|
||||
local_random = world.slot_seeds[player]
|
||||
|
||||
# progressive bow silver arrow hint hack
|
||||
prog_bow_locs = world.find_items('Progressive Bow', player)
|
||||
if len(prog_bow_locs) > 1:
|
||||
# only pick a distingushed bow if we have at least two
|
||||
distinguished_prog_bow_loc = local_random.choice(prog_bow_locs)
|
||||
distinguished_prog_bow_loc.item.code = 0x65
|
||||
|
||||
# patch items
|
||||
|
||||
for location in world.get_locations():
|
||||
@@ -785,7 +778,7 @@ def patch_rom(world, rom, player, enemized):
|
||||
itemid = 0x33
|
||||
elif location.item.compass:
|
||||
itemid = 0x25
|
||||
if world.worlds[player].remote_items: # remote items does not currently work
|
||||
if world.worlds[player].remote_items: # remote items does not currently work
|
||||
itemid = list(location_table.keys()).index(location.name) + 1
|
||||
assert itemid < 0x100
|
||||
rom.write_byte(location.player_address, 0xFF)
|
||||
@@ -1495,7 +1488,8 @@ def patch_rom(world, rom, player, enemized):
|
||||
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
|
||||
| (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
|
||||
@@ -1550,7 +1544,8 @@ def patch_rom(world, rom, player, enemized):
|
||||
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.smallkey_shuffle[player] == smallkey_shuffle.option_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
|
||||
@@ -2178,7 +2173,8 @@ def write_strings(rom, world, player):
|
||||
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
|
||||
else:
|
||||
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
|
||||
hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 0
|
||||
hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull',
|
||||
'dungeonscrossed'] else 0
|
||||
for entrance in all_entrances:
|
||||
if entrance.name in entrances_to_hint:
|
||||
if hint_count:
|
||||
@@ -2195,7 +2191,8 @@ def write_strings(rom, world, player):
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
|
||||
locations_to_hint.extend(InconvenientVanillaLocations)
|
||||
local_random.shuffle(locations_to_hint)
|
||||
hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 5
|
||||
hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull',
|
||||
'dungeonscrossed'] else 5
|
||||
for location in locations_to_hint[:hint_count]:
|
||||
if location == 'Swamp Left':
|
||||
if local_random.randint(0, 1):
|
||||
@@ -2254,7 +2251,8 @@ def write_strings(rom, world, 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
|
||||
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull',
|
||||
'dungeonscrossed'] else 8
|
||||
while hint_count > 0 and items_to_hint:
|
||||
this_item = items_to_hint.pop(0)
|
||||
this_location = world.find_items(this_item, player)
|
||||
@@ -2278,21 +2276,22 @@ def write_strings(rom, world, player):
|
||||
' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!'
|
||||
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
|
||||
prog_bow_locs = world.find_items('Progressive Bow', player)
|
||||
distinguished_prog_bow_loc = next((location for location in prog_bow_locs if location.item.code == 0x65), None)
|
||||
progressive_silvers = world.difficulty_requirements[player].progressive_bow_limit >= 2 or (
|
||||
world.swordless[player] or world.logic[player] == 'noglitches')
|
||||
if distinguished_prog_bow_loc:
|
||||
prog_bow_locs.remove(distinguished_prog_bow_loc)
|
||||
silverarrow_hint = (' %s?' % hint_text(distinguished_prog_bow_loc).replace('Ganon\'s',
|
||||
'my')) if progressive_silvers else '?\nI think not!'
|
||||
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
|
||||
if any(prog_bow_locs):
|
||||
silverarrow_hint = (' %s?' % hint_text(local_random.choice(prog_bow_locs)).replace('Ganon\'s',
|
||||
'my')) if progressive_silvers else '?\nI think not!'
|
||||
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
if world.worlds[player].has_progressive_bows and (world.difficulty_requirements[player].progressive_bow_limit >= 2 or (
|
||||
world.swordless[player] or world.logic[player] == 'noglitches')):
|
||||
prog_bow_locs = world.find_items('Progressive Bow', player)
|
||||
world.slot_seeds[player].shuffle(prog_bow_locs)
|
||||
found_bow = False
|
||||
found_bow_alt = False
|
||||
while prog_bow_locs and not (found_bow and found_bow_alt):
|
||||
bow_loc = prog_bow_locs.pop()
|
||||
if bow_loc.item.code == 0x65:
|
||||
found_bow_alt = True
|
||||
target = 'ganon_phase_3_no_silvers'
|
||||
else:
|
||||
found_bow = True
|
||||
target = 'ganon_phase_3_no_silvers_alt'
|
||||
silverarrow_hint = (' %s?' % hint_text(bow_loc).replace('Ganon\'s', 'my'))
|
||||
tt[target] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
|
||||
crystal5 = world.find_item('Crystal 5', player)
|
||||
crystal6 = world.find_item('Crystal 6', player)
|
||||
@@ -2953,4 +2952,4 @@ def get_base_rom_path(file_name: str = "") -> str:
|
||||
file_name = options["lttp_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.local_path(file_name)
|
||||
return file_name
|
||||
return file_name
|
||||
|
||||
@@ -27,8 +27,8 @@ def set_rules(world):
|
||||
else:
|
||||
# Set access rules according to max glitches for multiworld progression.
|
||||
# Set accessibility to none, and shuffle assuming the no logic players can always win
|
||||
world.accessibility[player] = 'none'
|
||||
world.progression_balancing[player] = False
|
||||
world.accessibility[player] = world.accessibility[player].from_text("minimal")
|
||||
world.progression_balancing[player].value = False
|
||||
|
||||
else:
|
||||
world.completion_condition[player] = lambda state: state.has('Triforce', player)
|
||||
|
||||
@@ -14,7 +14,8 @@ from .Rules import set_rules
|
||||
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
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string, \
|
||||
get_base_rom_path
|
||||
import Patch
|
||||
|
||||
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
@@ -41,6 +42,7 @@ class ALTTPWorld(World):
|
||||
|
||||
data_version = 8
|
||||
remote_items: bool = False
|
||||
remote_start_inventory: bool = False
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
@@ -50,6 +52,7 @@ class ALTTPWorld(World):
|
||||
self.dungeon_local_item_names = set()
|
||||
self.dungeon_specific_item_names = set()
|
||||
self.rom_name_available_event = threading.Event()
|
||||
self.has_progressive_bows = False
|
||||
super(ALTTPWorld, self).__init__(*args, **kwargs)
|
||||
|
||||
def generate_early(self):
|
||||
@@ -74,9 +77,9 @@ class ALTTPWorld(World):
|
||||
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]
|
||||
world.local_items[player].value |= 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]
|
||||
world.non_local_items[player].value |= 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":
|
||||
@@ -258,10 +261,10 @@ class ALTTPWorld(World):
|
||||
try:
|
||||
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
|
||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||
or world.shufflepots[player] or world.bush_shuffle[player]
|
||||
or world.pot_shuffle[player] or world.bush_shuffle[player]
|
||||
or world.killable_thieves[player])
|
||||
|
||||
rom = LocalRom(world.alttp_rom)
|
||||
rom = LocalRom(get_base_rom_path())
|
||||
|
||||
patch_rom(world, rom, player, use_enemizer)
|
||||
|
||||
@@ -298,7 +301,7 @@ class ALTTPWorld(World):
|
||||
if world.player_name[player] != 'Player%d' % player else ''
|
||||
|
||||
rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc')
|
||||
rom.write_to_file(rompath, hide_enemizer=True)
|
||||
rom.write_to_file(rompath)
|
||||
Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player])
|
||||
os.unlink(rompath)
|
||||
self.rom_name = rom.name
|
||||
|
||||
@@ -185,6 +185,11 @@ all_product_sources: Dict[str, Set[Recipe]] = {"character": set()}
|
||||
raw_recipes["uranium-ore"] = {"ingredients": {"sulfuric-acid": 1}, "products": {"uranium-ore": 1}, "category": "mining",
|
||||
"energy": 2}
|
||||
|
||||
# raw_recipes["iron-ore"] = {"ingredients": {}, "products": {"iron-ore": 1}, "category": "mining", "energy": 2}
|
||||
# raw_recipes["copper-ore"] = {"ingredients": {}, "products": {"copper-ore": 1}, "category": "mining", "energy": 2}
|
||||
# raw_recipes["coal-ore"] = {"ingredients": {}, "products": {"coal": 1}, "category": "mining", "energy": 2}
|
||||
# raw_recipes["stone"] = {"ingredients": {}, "products": {"coal": 1}, "category": "mining", "energy": 2}
|
||||
|
||||
for recipe_name, recipe_data in raw_recipes.items():
|
||||
# example:
|
||||
# "accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting"}
|
||||
@@ -473,6 +478,8 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
|
||||
if (science_pack != "automation-science-pack" or not recipe.recursive_unlocking_technologies) \
|
||||
and get_estimated_difficulty(recipe) < current_difficulty:
|
||||
current |= set(recipe.products)
|
||||
if science_pack == "automation-science-pack":
|
||||
current |= {"iron-ore", "copper-ore", "coal", "stone"}
|
||||
current -= already_taken
|
||||
already_taken |= current
|
||||
current_difficulty *= 2
|
||||
|
||||
@@ -113,7 +113,7 @@ class Factorio(World):
|
||||
if self.world.recipe_ingredients[self.player]:
|
||||
custom_recipe = self.custom_recipes[ingredient]
|
||||
|
||||
location.access_rule = lambda state, ingredient=ingredient, custom_recipe = custom_recipe: \
|
||||
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
|
||||
(ingredient not in technology_table or state.has(ingredient, player)) and \
|
||||
all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients
|
||||
for technology in required_technologies[sub_ingredient])
|
||||
@@ -134,9 +134,9 @@ class Factorio(World):
|
||||
locations=locations: all(state.can_reach(loc) for loc in locations))
|
||||
|
||||
silo_recipe = None if self.world.silo[self.player].value == Silo.option_spawn \
|
||||
else self.custom_recipes["rocket-silo"] \
|
||||
if "rocket-silo" in self.custom_recipes \
|
||||
else next(iter(all_product_sources.get("rocket-silo")))
|
||||
else self.custom_recipes["rocket-silo"] \
|
||||
if "rocket-silo" in self.custom_recipes \
|
||||
else next(iter(all_product_sources.get("rocket-silo")))
|
||||
part_recipe = self.custom_recipes["rocket-part"]
|
||||
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe)
|
||||
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
|
||||
@@ -189,19 +189,24 @@ class Factorio(World):
|
||||
max_energy = remaining_energy * 0.75
|
||||
min_energy = (remaining_energy - max_energy) / remaining_num_ingredients
|
||||
ingredient = pool.pop()
|
||||
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
|
||||
max_num_raw = max_raw/ingredient_raw
|
||||
min_num_energy = min_energy/ingredient_energy
|
||||
max_num_energy = max_energy/ingredient_energy
|
||||
if ingredient in all_product_sources:
|
||||
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
|
||||
else:
|
||||
# assume simple ore TODO: remove if tree when mining data is harvested from Factorio
|
||||
ingredient_raw = 1
|
||||
ingredient_energy = 2
|
||||
min_num_raw = min_raw / ingredient_raw
|
||||
max_num_raw = max_raw / ingredient_raw
|
||||
min_num_energy = min_energy / ingredient_energy
|
||||
max_num_energy = max_energy / ingredient_energy
|
||||
min_num = int(max(1, min_num_raw, min_num_energy))
|
||||
max_num = int(min(1000, max_num_raw, max_num_energy))
|
||||
if min_num > max_num:
|
||||
fallback_pool.append(ingredient)
|
||||
continue # can't use that ingredient
|
||||
num = self.world.random.randint(min_num,max_num)
|
||||
continue # can't use that ingredient
|
||||
num = self.world.random.randint(min_num, max_num)
|
||||
new_ingredients[ingredient] = num
|
||||
remaining_raw -= num * ingredient_raw
|
||||
remaining_energy -= num * ingredient_energy
|
||||
@@ -217,8 +222,8 @@ class Factorio(World):
|
||||
ingredient_recipe = recipes[ingredient]
|
||||
ingredient_raw = sum((count for ingredient, count in ingredient_recipe.base_cost.items()))
|
||||
ingredient_energy = ingredient_recipe.total_energy
|
||||
num_raw = remaining_raw/ingredient_raw/remaining_num_ingredients
|
||||
num_energy = remaining_energy/ingredient_energy/remaining_num_ingredients
|
||||
num_raw = remaining_raw / ingredient_raw / remaining_num_ingredients
|
||||
num_energy = remaining_energy / ingredient_energy / remaining_num_ingredients
|
||||
num = int(min(num_raw, num_energy))
|
||||
if num < 1: continue
|
||||
new_ingredients[ingredient] = num
|
||||
@@ -244,7 +249,7 @@ class Factorio(World):
|
||||
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()])
|
||||
self.world.random.shuffle(valid_pool)
|
||||
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
|
||||
{valid_pool[x] : 10 for x in range(3)},
|
||||
{valid_pool[x]: 10 for x in range(3)},
|
||||
original_rocket_part.products,
|
||||
original_rocket_part.energy)}
|
||||
self.additional_advancement_technologies = {tech.name for tech in
|
||||
@@ -255,7 +260,7 @@ class Factorio(World):
|
||||
for pack in self.world.max_science_pack[self.player].get_ordered_science_packs():
|
||||
valid_pool += sorted(science_pack_pools[pack])
|
||||
self.world.random.shuffle(valid_pool)
|
||||
if pack in recipes: # skips over space science pack
|
||||
if pack in recipes: # skips over space science pack
|
||||
original = recipes[pack]
|
||||
new_ingredients = {}
|
||||
for _ in original.ingredients:
|
||||
@@ -270,7 +275,7 @@ class Factorio(World):
|
||||
for pack in self.world.max_science_pack[self.player].get_allowed_packs():
|
||||
valid_pool += sorted(science_pack_pools[pack])
|
||||
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool,
|
||||
factor = (self.world.max_science_pack[self.player].value+1)/7)
|
||||
factor=(self.world.max_science_pack[self.player].value + 1) / 7)
|
||||
self.additional_advancement_technologies |= {tech.name for tech in
|
||||
new_recipe.recursive_unlocking_technologies}
|
||||
self.custom_recipes["rocket-silo"] = new_recipe
|
||||
|
||||
@@ -127,24 +127,29 @@ function update_player(index)
|
||||
for name, count in pairs(samples) do
|
||||
stack.name = name
|
||||
stack.count = count
|
||||
if character.can_insert(stack) then
|
||||
sent = character.insert(stack)
|
||||
else
|
||||
sent = 0
|
||||
end
|
||||
if sent > 0 then
|
||||
player.print("Received " .. sent .. "x [item=" .. name .. "]")
|
||||
data.suppress_full_inventory_message = false
|
||||
end
|
||||
if sent ~= count then -- Couldn't full send.
|
||||
if not data.suppress_full_inventory_message then
|
||||
player.print("Additional items will be sent when inventory space is available.", {r=1, g=1, b=0.25})
|
||||
if game.item_prototypes[name] then
|
||||
if character.can_insert(stack) then
|
||||
sent = character.insert(stack)
|
||||
else
|
||||
sent = 0
|
||||
end
|
||||
if sent > 0 then
|
||||
player.print("Received " .. sent .. "x [item=" .. name .. "]")
|
||||
data.suppress_full_inventory_message = false
|
||||
end
|
||||
if sent ~= count then -- Couldn't full send.
|
||||
if not data.suppress_full_inventory_message then
|
||||
player.print("Additional items will be sent when inventory space is available.", {r=1, g=1, b=0.25})
|
||||
end
|
||||
data.suppress_full_inventory_message = true -- Avoid spamming them with repeated full inventory messages.
|
||||
samples[name] = count - sent -- Buffer the remaining items
|
||||
break -- Stop trying to send other things
|
||||
else
|
||||
samples[name] = nil -- Remove from the list
|
||||
end
|
||||
data.suppress_full_inventory_message = true -- Avoid spamming them with repeated full inventory messages.
|
||||
samples[name] = count - sent -- Buffer the remaining items
|
||||
break -- Stop trying to send other things
|
||||
else
|
||||
samples[name] = nil -- Remove from the list
|
||||
player.print("Unable to receive " .. count .. "x [item=" .. name .. "] as this item does not exist.")
|
||||
samples[name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
def locality_rules(world, player):
|
||||
if world.local_items[player]:
|
||||
if world.local_items[player].value:
|
||||
for location in world.get_locations():
|
||||
if location.player != player:
|
||||
forbid_items_for_player(location, world.local_items[player], player)
|
||||
if world.non_local_items[player]:
|
||||
forbid_items_for_player(location, world.local_items[player].value, player)
|
||||
if world.non_local_items[player].value:
|
||||
for location in world.get_locations():
|
||||
if location.player == player:
|
||||
forbid_items_for_player(location, world.non_local_items[player], player)
|
||||
forbid_items_for_player(location, world.non_local_items[player].value, player)
|
||||
|
||||
|
||||
def exclusion_rules(world, player: int, excluded_locations: set):
|
||||
for loc_name in excluded_locations:
|
||||
def exclusion_rules(world, player: int, exclude_locations: set):
|
||||
for loc_name in exclude_locations:
|
||||
location = world.get_location(loc_name, player)
|
||||
add_item_rule(location, lambda i: not (i.advancement or i.never_exclude))
|
||||
location.excluded = True
|
||||
|
||||
963
worlds/oot/ColorSFXOptions.py
Normal file
@@ -0,0 +1,963 @@
|
||||
# Auto-generated color and sound-effect options from Colors.py and Sounds.py
|
||||
from Options import Choice
|
||||
|
||||
|
||||
class kokiri_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Kokiri Tunic"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_kokiri_green = 2
|
||||
option_goron_red = 3
|
||||
option_zora_blue = 4
|
||||
option_black = 5
|
||||
option_white = 6
|
||||
option_azure_blue = 7
|
||||
option_vivid_cyan = 8
|
||||
option_light_red = 9
|
||||
option_fuchsia = 10
|
||||
option_purple = 11
|
||||
option_majora_purple = 12
|
||||
option_twitch_purple = 13
|
||||
option_purple_heart = 14
|
||||
option_persian_rose = 15
|
||||
option_dirty_yellow = 16
|
||||
option_blush_pink = 17
|
||||
option_hot_pink = 18
|
||||
option_rose_pink = 19
|
||||
option_orange = 20
|
||||
option_gray = 21
|
||||
option_gold = 22
|
||||
option_silver = 23
|
||||
option_beige = 24
|
||||
option_teal = 25
|
||||
option_blood_red = 26
|
||||
option_blood_orange = 27
|
||||
option_royal_blue = 28
|
||||
option_sonic_blue = 29
|
||||
option_nes_green = 30
|
||||
option_dark_green = 31
|
||||
option_lumen = 32
|
||||
default = 2
|
||||
|
||||
|
||||
class goron_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Goron Tunic"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_kokiri_green = 2
|
||||
option_goron_red = 3
|
||||
option_zora_blue = 4
|
||||
option_black = 5
|
||||
option_white = 6
|
||||
option_azure_blue = 7
|
||||
option_vivid_cyan = 8
|
||||
option_light_red = 9
|
||||
option_fuchsia = 10
|
||||
option_purple = 11
|
||||
option_majora_purple = 12
|
||||
option_twitch_purple = 13
|
||||
option_purple_heart = 14
|
||||
option_persian_rose = 15
|
||||
option_dirty_yellow = 16
|
||||
option_blush_pink = 17
|
||||
option_hot_pink = 18
|
||||
option_rose_pink = 19
|
||||
option_orange = 20
|
||||
option_gray = 21
|
||||
option_gold = 22
|
||||
option_silver = 23
|
||||
option_beige = 24
|
||||
option_teal = 25
|
||||
option_blood_red = 26
|
||||
option_blood_orange = 27
|
||||
option_royal_blue = 28
|
||||
option_sonic_blue = 29
|
||||
option_nes_green = 30
|
||||
option_dark_green = 31
|
||||
option_lumen = 32
|
||||
default = 3
|
||||
|
||||
|
||||
class zora_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Zora Tunic"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_kokiri_green = 2
|
||||
option_goron_red = 3
|
||||
option_zora_blue = 4
|
||||
option_black = 5
|
||||
option_white = 6
|
||||
option_azure_blue = 7
|
||||
option_vivid_cyan = 8
|
||||
option_light_red = 9
|
||||
option_fuchsia = 10
|
||||
option_purple = 11
|
||||
option_majora_purple = 12
|
||||
option_twitch_purple = 13
|
||||
option_purple_heart = 14
|
||||
option_persian_rose = 15
|
||||
option_dirty_yellow = 16
|
||||
option_blush_pink = 17
|
||||
option_hot_pink = 18
|
||||
option_rose_pink = 19
|
||||
option_orange = 20
|
||||
option_gray = 21
|
||||
option_gold = 22
|
||||
option_silver = 23
|
||||
option_beige = 24
|
||||
option_teal = 25
|
||||
option_blood_red = 26
|
||||
option_blood_orange = 27
|
||||
option_royal_blue = 28
|
||||
option_sonic_blue = 29
|
||||
option_nes_green = 30
|
||||
option_dark_green = 31
|
||||
option_lumen = 32
|
||||
default = 4
|
||||
|
||||
|
||||
class silver_gauntlets_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Silver Gauntlets Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_silver = 2
|
||||
option_gold = 3
|
||||
option_black = 4
|
||||
option_green = 5
|
||||
option_blue = 6
|
||||
option_bronze = 7
|
||||
option_red = 8
|
||||
option_sky_blue = 9
|
||||
option_pink = 10
|
||||
option_magenta = 11
|
||||
option_orange = 12
|
||||
option_lime = 13
|
||||
option_purple = 14
|
||||
default = 2
|
||||
|
||||
|
||||
class golden_gauntlets_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Golden Gauntlets Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_silver = 2
|
||||
option_gold = 3
|
||||
option_black = 4
|
||||
option_green = 5
|
||||
option_blue = 6
|
||||
option_bronze = 7
|
||||
option_red = 8
|
||||
option_sky_blue = 9
|
||||
option_pink = 10
|
||||
option_magenta = 11
|
||||
option_orange = 12
|
||||
option_lime = 13
|
||||
option_purple = 14
|
||||
default = 3
|
||||
|
||||
|
||||
class mirror_shield_frame_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Mirror Shield Frame Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_red = 2
|
||||
option_green = 3
|
||||
option_blue = 4
|
||||
option_yellow = 5
|
||||
option_cyan = 6
|
||||
option_magenta = 7
|
||||
option_orange = 8
|
||||
option_gold = 9
|
||||
option_purple = 10
|
||||
option_pink = 11
|
||||
default = 2
|
||||
|
||||
|
||||
class navi_color_default_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Navi Idle Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_gold = 3
|
||||
option_white = 4
|
||||
option_green = 5
|
||||
option_light_blue = 6
|
||||
option_yellow = 7
|
||||
option_red = 8
|
||||
option_magenta = 9
|
||||
option_black = 10
|
||||
option_tatl = 11
|
||||
option_tael = 12
|
||||
option_fi = 13
|
||||
option_ciela = 14
|
||||
option_epona = 15
|
||||
option_ezlo = 16
|
||||
option_king_of_red_lions = 17
|
||||
option_linebeck = 18
|
||||
option_loftwing = 19
|
||||
option_midna = 20
|
||||
option_phantom_zelda = 21
|
||||
default = 4
|
||||
|
||||
|
||||
class navi_color_default_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Navi Idle Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_gold = 3
|
||||
option_white = 4
|
||||
option_green = 5
|
||||
option_light_blue = 6
|
||||
option_yellow = 7
|
||||
option_red = 8
|
||||
option_magenta = 9
|
||||
option_black = 10
|
||||
option_tatl = 11
|
||||
option_tael = 12
|
||||
option_fi = 13
|
||||
option_ciela = 14
|
||||
option_epona = 15
|
||||
option_ezlo = 16
|
||||
option_king_of_red_lions = 17
|
||||
option_linebeck = 18
|
||||
option_loftwing = 19
|
||||
option_midna = 20
|
||||
option_phantom_zelda = 21
|
||||
option_match_inner = 22
|
||||
default = 22
|
||||
|
||||
|
||||
class navi_color_enemy_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Navi Targeting Enemy Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_gold = 3
|
||||
option_white = 4
|
||||
option_green = 5
|
||||
option_light_blue = 6
|
||||
option_yellow = 7
|
||||
option_red = 8
|
||||
option_magenta = 9
|
||||
option_black = 10
|
||||
option_tatl = 11
|
||||
option_tael = 12
|
||||
option_fi = 13
|
||||
option_ciela = 14
|
||||
option_epona = 15
|
||||
option_ezlo = 16
|
||||
option_king_of_red_lions = 17
|
||||
option_linebeck = 18
|
||||
option_loftwing = 19
|
||||
option_midna = 20
|
||||
option_phantom_zelda = 21
|
||||
default = 7
|
||||
|
||||
|
||||
class navi_color_enemy_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Navi Targeting Enemy Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_gold = 3
|
||||
option_white = 4
|
||||
option_green = 5
|
||||
option_light_blue = 6
|
||||
option_yellow = 7
|
||||
option_red = 8
|
||||
option_magenta = 9
|
||||
option_black = 10
|
||||
option_tatl = 11
|
||||
option_tael = 12
|
||||
option_fi = 13
|
||||
option_ciela = 14
|
||||
option_epona = 15
|
||||
option_ezlo = 16
|
||||
option_king_of_red_lions = 17
|
||||
option_linebeck = 18
|
||||
option_loftwing = 19
|
||||
option_midna = 20
|
||||
option_phantom_zelda = 21
|
||||
option_match_inner = 22
|
||||
default = 22
|
||||
|
||||
|
||||
class navi_color_npc_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Navi Targeting NPC Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_gold = 3
|
||||
option_white = 4
|
||||
option_green = 5
|
||||
option_light_blue = 6
|
||||
option_yellow = 7
|
||||
option_red = 8
|
||||
option_magenta = 9
|
||||
option_black = 10
|
||||
option_tatl = 11
|
||||
option_tael = 12
|
||||
option_fi = 13
|
||||
option_ciela = 14
|
||||
option_epona = 15
|
||||
option_ezlo = 16
|
||||
option_king_of_red_lions = 17
|
||||
option_linebeck = 18
|
||||
option_loftwing = 19
|
||||
option_midna = 20
|
||||
option_phantom_zelda = 21
|
||||
default = 6
|
||||
|
||||
|
||||
class navi_color_npc_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Navi Targeting NPC Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_gold = 3
|
||||
option_white = 4
|
||||
option_green = 5
|
||||
option_light_blue = 6
|
||||
option_yellow = 7
|
||||
option_red = 8
|
||||
option_magenta = 9
|
||||
option_black = 10
|
||||
option_tatl = 11
|
||||
option_tael = 12
|
||||
option_fi = 13
|
||||
option_ciela = 14
|
||||
option_epona = 15
|
||||
option_ezlo = 16
|
||||
option_king_of_red_lions = 17
|
||||
option_linebeck = 18
|
||||
option_loftwing = 19
|
||||
option_midna = 20
|
||||
option_phantom_zelda = 21
|
||||
option_match_inner = 22
|
||||
default = 22
|
||||
|
||||
|
||||
class navi_color_prop_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Navi Targeting Prop Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_gold = 3
|
||||
option_white = 4
|
||||
option_green = 5
|
||||
option_light_blue = 6
|
||||
option_yellow = 7
|
||||
option_red = 8
|
||||
option_magenta = 9
|
||||
option_black = 10
|
||||
option_tatl = 11
|
||||
option_tael = 12
|
||||
option_fi = 13
|
||||
option_ciela = 14
|
||||
option_epona = 15
|
||||
option_ezlo = 16
|
||||
option_king_of_red_lions = 17
|
||||
option_linebeck = 18
|
||||
option_loftwing = 19
|
||||
option_midna = 20
|
||||
option_phantom_zelda = 21
|
||||
default = 5
|
||||
|
||||
|
||||
class navi_color_prop_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Navi Targeting Prop Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_gold = 3
|
||||
option_white = 4
|
||||
option_green = 5
|
||||
option_light_blue = 6
|
||||
option_yellow = 7
|
||||
option_red = 8
|
||||
option_magenta = 9
|
||||
option_black = 10
|
||||
option_tatl = 11
|
||||
option_tael = 12
|
||||
option_fi = 13
|
||||
option_ciela = 14
|
||||
option_epona = 15
|
||||
option_ezlo = 16
|
||||
option_king_of_red_lions = 17
|
||||
option_linebeck = 18
|
||||
option_loftwing = 19
|
||||
option_midna = 20
|
||||
option_phantom_zelda = 21
|
||||
option_match_inner = 22
|
||||
default = 22
|
||||
|
||||
|
||||
class sword_trail_color_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Sword Trail Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_white = 3
|
||||
option_red = 4
|
||||
option_green = 5
|
||||
option_blue = 6
|
||||
option_cyan = 7
|
||||
option_magenta = 8
|
||||
option_orange = 9
|
||||
option_gold = 10
|
||||
option_purple = 11
|
||||
option_pink = 12
|
||||
default = 3
|
||||
|
||||
|
||||
class sword_trail_color_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Sword Trail Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_white = 3
|
||||
option_red = 4
|
||||
option_green = 5
|
||||
option_blue = 6
|
||||
option_cyan = 7
|
||||
option_magenta = 8
|
||||
option_orange = 9
|
||||
option_gold = 10
|
||||
option_purple = 11
|
||||
option_pink = 12
|
||||
option_match_inner = 13
|
||||
default = 13
|
||||
|
||||
|
||||
class bombchu_trail_color_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Bombchu Trail Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_red = 3
|
||||
option_green = 4
|
||||
option_blue = 5
|
||||
option_cyan = 6
|
||||
option_magenta = 7
|
||||
option_orange = 8
|
||||
option_gold = 9
|
||||
option_purple = 10
|
||||
option_pink = 11
|
||||
default = 3
|
||||
|
||||
|
||||
class bombchu_trail_color_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Bombchu Trail Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_red = 3
|
||||
option_green = 4
|
||||
option_blue = 5
|
||||
option_cyan = 6
|
||||
option_magenta = 7
|
||||
option_orange = 8
|
||||
option_gold = 9
|
||||
option_purple = 10
|
||||
option_pink = 11
|
||||
option_match_inner = 12
|
||||
default = 12
|
||||
|
||||
|
||||
class boomerang_trail_color_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Boomerang Trail Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_yellow = 3
|
||||
option_red = 4
|
||||
option_green = 5
|
||||
option_blue = 6
|
||||
option_cyan = 7
|
||||
option_magenta = 8
|
||||
option_orange = 9
|
||||
option_gold = 10
|
||||
option_purple = 11
|
||||
option_pink = 12
|
||||
default = 3
|
||||
|
||||
|
||||
class boomerang_trail_color_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Boomerang Trail Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
option_yellow = 3
|
||||
option_red = 4
|
||||
option_green = 5
|
||||
option_blue = 6
|
||||
option_cyan = 7
|
||||
option_magenta = 8
|
||||
option_orange = 9
|
||||
option_gold = 10
|
||||
option_purple = 11
|
||||
option_pink = 12
|
||||
option_match_inner = 13
|
||||
default = 13
|
||||
|
||||
|
||||
class heart_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Heart Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_red = 2
|
||||
option_green = 3
|
||||
option_blue = 4
|
||||
option_yellow = 5
|
||||
default = 2
|
||||
|
||||
|
||||
class magic_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Magic Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_green = 2
|
||||
option_red = 3
|
||||
option_blue = 4
|
||||
option_purple = 5
|
||||
option_pink = 6
|
||||
option_yellow = 7
|
||||
option_white = 8
|
||||
default = 2
|
||||
|
||||
|
||||
class a_button_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "A Button Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_n64_blue = 2
|
||||
option_n64_green = 3
|
||||
option_n64_red = 4
|
||||
option_gamecube_green = 5
|
||||
option_gamecube_red = 6
|
||||
option_gamecube_grey = 7
|
||||
option_yellow = 8
|
||||
option_black = 9
|
||||
option_white = 10
|
||||
option_magenta = 11
|
||||
option_ruby = 12
|
||||
option_sapphire = 13
|
||||
option_lime = 14
|
||||
option_cyan = 15
|
||||
option_purple = 16
|
||||
option_orange = 17
|
||||
default = 2
|
||||
|
||||
|
||||
class b_button_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "B Button Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_n64_blue = 2
|
||||
option_n64_green = 3
|
||||
option_n64_red = 4
|
||||
option_gamecube_green = 5
|
||||
option_gamecube_red = 6
|
||||
option_gamecube_grey = 7
|
||||
option_yellow = 8
|
||||
option_black = 9
|
||||
option_white = 10
|
||||
option_magenta = 11
|
||||
option_ruby = 12
|
||||
option_sapphire = 13
|
||||
option_lime = 14
|
||||
option_cyan = 15
|
||||
option_purple = 16
|
||||
option_orange = 17
|
||||
default = 3
|
||||
|
||||
|
||||
class c_button_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "C Button Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_n64_blue = 2
|
||||
option_n64_green = 3
|
||||
option_n64_red = 4
|
||||
option_gamecube_green = 5
|
||||
option_gamecube_red = 6
|
||||
option_gamecube_grey = 7
|
||||
option_yellow = 8
|
||||
option_black = 9
|
||||
option_white = 10
|
||||
option_magenta = 11
|
||||
option_ruby = 12
|
||||
option_sapphire = 13
|
||||
option_lime = 14
|
||||
option_cyan = 15
|
||||
option_purple = 16
|
||||
option_orange = 17
|
||||
default = 8
|
||||
|
||||
|
||||
class start_button_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Start Button Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_n64_blue = 2
|
||||
option_n64_green = 3
|
||||
option_n64_red = 4
|
||||
option_gamecube_green = 5
|
||||
option_gamecube_red = 6
|
||||
option_gamecube_grey = 7
|
||||
option_yellow = 8
|
||||
option_black = 9
|
||||
option_white = 10
|
||||
option_magenta = 11
|
||||
option_ruby = 12
|
||||
option_sapphire = 13
|
||||
option_lime = 14
|
||||
option_cyan = 15
|
||||
option_purple = 16
|
||||
option_orange = 17
|
||||
default = 4
|
||||
|
||||
|
||||
class sfx_navi_overworld(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 = "Navi Overworld"
|
||||
option_default = 0
|
||||
option_completely_random = 1
|
||||
option_random_ear_safe = 2
|
||||
option_random_choice = 3
|
||||
option_none = 4
|
||||
option_bark = 5
|
||||
option_business_scrub = 6
|
||||
option_carrot_refill = 7
|
||||
option_cluck = 8
|
||||
option_cockadoodledoo = 9
|
||||
option_dusk_howl = 10
|
||||
option_exploding_crate = 11
|
||||
option_explosion = 12
|
||||
option_great_fairy = 13
|
||||
option_guay = 14
|
||||
option_low_health = 15
|
||||
option_recover_health = 16
|
||||
option_horse_neigh = 17
|
||||
option_shattering_ice = 18
|
||||
option_moo = 19
|
||||
option_mweep = 20
|
||||
option_navi_hello = 21
|
||||
option_notification = 22
|
||||
option_poe = 23
|
||||
option_shattering_pot = 24
|
||||
option_redead_scream = 25
|
||||
option_ribbit = 26
|
||||
option_ruto_giggle = 27
|
||||
option_skulltula = 28
|
||||
option_soft_beep = 29
|
||||
option_tambourine = 30
|
||||
option_timer = 31
|
||||
option_adult_zelda_gasp = 32
|
||||
|
||||
|
||||
|
||||
class sfx_navi_enemy(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 = "Navi Enemy"
|
||||
option_default = 0
|
||||
option_completely_random = 1
|
||||
option_random_ear_safe = 2
|
||||
option_random_choice = 3
|
||||
option_none = 4
|
||||
option_bark = 5
|
||||
option_business_scrub = 6
|
||||
option_carrot_refill = 7
|
||||
option_cluck = 8
|
||||
option_cockadoodledoo = 9
|
||||
option_dusk_howl = 10
|
||||
option_exploding_crate = 11
|
||||
option_explosion = 12
|
||||
option_great_fairy = 13
|
||||
option_guay = 14
|
||||
option_low_health = 15
|
||||
option_recover_health = 16
|
||||
option_horse_neigh = 17
|
||||
option_shattering_ice = 18
|
||||
option_moo = 19
|
||||
option_mweep = 20
|
||||
option_navi_hello = 21
|
||||
option_notification = 22
|
||||
option_poe = 23
|
||||
option_shattering_pot = 24
|
||||
option_redead_scream = 25
|
||||
option_ribbit = 26
|
||||
option_ruto_giggle = 27
|
||||
option_skulltula = 28
|
||||
option_soft_beep = 29
|
||||
option_tambourine = 30
|
||||
option_timer = 31
|
||||
option_adult_zelda_gasp = 32
|
||||
|
||||
|
||||
|
||||
class sfx_low_hp(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 = "Low HP"
|
||||
option_default = 0
|
||||
option_completely_random = 1
|
||||
option_random_ear_safe = 2
|
||||
option_random_choice = 3
|
||||
option_none = 4
|
||||
option_bark = 5
|
||||
option_bomb_bounce = 6
|
||||
option_bongo_bongo_low = 7
|
||||
option_bow_twang = 8
|
||||
option_business_scrub = 9
|
||||
option_carrot_refill = 10
|
||||
option_cluck = 11
|
||||
option_drawbridge_set = 12
|
||||
option_guay = 13
|
||||
option_recover_health = 14
|
||||
option_horse_trot = 15
|
||||
option_iron_boots = 16
|
||||
option_moo = 17
|
||||
option_mweep = 18
|
||||
option_navi_hey = 19
|
||||
option_navi_random = 20
|
||||
option_notification = 21
|
||||
option_shattering_pot = 22
|
||||
option_ribbit = 23
|
||||
option_silver_rupee = 24
|
||||
option_soft_beep = 25
|
||||
option_switch = 26
|
||||
option_sword_bonk = 27
|
||||
option_tambourine = 28
|
||||
option_timer = 29
|
||||
option_adult_zelda_gasp = 30
|
||||
|
||||
|
||||
|
||||
class sfx_menu_cursor(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 = "Menu Cursor"
|
||||
option_default = 0
|
||||
option_completely_random = 1
|
||||
option_random_ear_safe = 2
|
||||
option_random_choice = 3
|
||||
option_none = 4
|
||||
option_bark = 5
|
||||
option_bomb_bounce = 6
|
||||
option_bongo_bongo_high = 7
|
||||
option_bongo_bongo_low = 8
|
||||
option_bottle_cork = 9
|
||||
option_bow_twang = 10
|
||||
option_bubble_laugh = 11
|
||||
option_carrot_refill = 12
|
||||
option_change_item = 13
|
||||
option_child_pant = 14
|
||||
option_cluck = 15
|
||||
option_deku_baba = 16
|
||||
option_drawbridge_set = 17
|
||||
option_dusk_howl = 18
|
||||
option_fanfare_light = 19
|
||||
option_fanfare_medium = 20
|
||||
option_field_shrub = 21
|
||||
option_flare_dancer_startled = 22
|
||||
option_ganondorf_teh = 23
|
||||
option_gohma_larva_croak = 24
|
||||
option_gold_skull_token = 25
|
||||
option_goron_wake = 26
|
||||
option_guay = 27
|
||||
option_gunshot = 28
|
||||
option_low_health = 29
|
||||
option_recover_health = 30
|
||||
option_hammer_bonk = 31
|
||||
option_horse_trot = 32
|
||||
option_iron_boots = 33
|
||||
option_iron_knuckle = 34
|
||||
option_moo = 35
|
||||
option_mweep = 36
|
||||
option_notification = 37
|
||||
option_phantom_ganon_laugh = 38
|
||||
option_plant_explode = 39
|
||||
option_shattering_pot = 40
|
||||
option_redead_moan = 41
|
||||
option_ribbit = 42
|
||||
option_rupee = 43
|
||||
option_silver_rupee = 44
|
||||
option_ruto_crash = 45
|
||||
option_ruto_lift = 46
|
||||
option_ruto_thrown = 47
|
||||
option_scrub_emerge = 48
|
||||
option_shabom_bounce = 49
|
||||
option_shabom_pop = 50
|
||||
option_shellblade = 51
|
||||
option_skulltula = 52
|
||||
option_soft_beep = 53
|
||||
option_spit_nut = 54
|
||||
option_switch = 55
|
||||
option_sword_bonk = 56
|
||||
option_talon_hmm = 57
|
||||
option_talon_snore = 58
|
||||
option_talon_wtf = 59
|
||||
option_tambourine = 60
|
||||
option_target_enemy = 61
|
||||
option_target_neutral = 62
|
||||
option_thunder = 63
|
||||
option_timer = 64
|
||||
option_adult_zelda_gasp = 65
|
||||
|
||||
|
||||
|
||||
class sfx_menu_select(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 = "Menu Select"
|
||||
option_default = 0
|
||||
option_completely_random = 1
|
||||
option_random_ear_safe = 2
|
||||
option_random_choice = 3
|
||||
option_none = 4
|
||||
option_bark = 5
|
||||
option_bomb_bounce = 6
|
||||
option_bongo_bongo_high = 7
|
||||
option_bongo_bongo_low = 8
|
||||
option_bottle_cork = 9
|
||||
option_bow_twang = 10
|
||||
option_bubble_laugh = 11
|
||||
option_carrot_refill = 12
|
||||
option_change_item = 13
|
||||
option_child_cringe = 14
|
||||
option_child_pant = 15
|
||||
option_child_scream = 16
|
||||
option_cluck = 17
|
||||
option_deku_baba = 18
|
||||
option_drawbridge_set = 19
|
||||
option_dusk_howl = 20
|
||||
option_fanfare_light = 21
|
||||
option_fanfare_medium = 22
|
||||
option_field_shrub = 23
|
||||
option_flare_dancer_startled = 24
|
||||
option_ganondorf_teh = 25
|
||||
option_gohma_larva_croak = 26
|
||||
option_gold_skull_token = 27
|
||||
option_goron_wake = 28
|
||||
option_guay = 29
|
||||
option_gunshot = 30
|
||||
option_low_health = 31
|
||||
option_recover_health = 32
|
||||
option_hammer_bonk = 33
|
||||
option_horse_trot = 34
|
||||
option_iron_boots = 35
|
||||
option_iron_knuckle = 36
|
||||
option_moo = 37
|
||||
option_mweep = 38
|
||||
option_notification = 39
|
||||
option_phantom_ganon_laugh = 40
|
||||
option_plant_explode = 41
|
||||
option_shattering_pot = 42
|
||||
option_redead_moan = 43
|
||||
option_ribbit = 44
|
||||
option_rupee = 45
|
||||
option_silver_rupee = 46
|
||||
option_ruto_crash = 47
|
||||
option_ruto_lift = 48
|
||||
option_ruto_thrown = 49
|
||||
option_scrub_emerge = 50
|
||||
option_shabom_bounce = 51
|
||||
option_shabom_pop = 52
|
||||
option_shellblade = 53
|
||||
option_skulltula = 54
|
||||
option_soft_beep = 55
|
||||
option_spit_nut = 56
|
||||
option_switch = 57
|
||||
option_sword_bonk = 58
|
||||
option_talon_hmm = 59
|
||||
option_talon_snore = 60
|
||||
option_talon_wtf = 61
|
||||
option_tambourine = 62
|
||||
option_target_enemy = 63
|
||||
option_target_neutral = 64
|
||||
option_thunder = 65
|
||||
option_timer = 66
|
||||
option_adult_zelda_gasp = 67
|
||||
|
||||
|
||||
|
||||
class sfx_nightfall(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 = "Nightfall"
|
||||
option_default = 0
|
||||
option_completely_random = 1
|
||||
option_random_ear_safe = 2
|
||||
option_random_choice = 3
|
||||
option_none = 4
|
||||
option_cockadoodledoo = 5
|
||||
option_gold_skull_token = 6
|
||||
option_great_fairy = 7
|
||||
option_moo = 8
|
||||
option_mweep = 9
|
||||
option_redead_moan = 10
|
||||
option_talon_snore = 11
|
||||
option_thunder = 12
|
||||
|
||||
|
||||
|
||||
class sfx_horse_neigh(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 = "Horse"
|
||||
option_default = 0
|
||||
option_completely_random = 1
|
||||
option_random_ear_safe = 2
|
||||
option_random_choice = 3
|
||||
option_none = 4
|
||||
option_armos = 5
|
||||
option_child_scream = 6
|
||||
option_great_fairy = 7
|
||||
option_moo = 8
|
||||
option_mweep = 9
|
||||
option_redead_scream = 10
|
||||
option_ruto_wiggle = 11
|
||||
option_stalchild_attack = 12
|
||||
|
||||
|
||||
|
||||
class sfx_hover_boots(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 = "Hover Boots"
|
||||
option_default = 0
|
||||
option_completely_random = 1
|
||||
option_random_ear_safe = 2
|
||||
option_random_choice = 3
|
||||
option_none = 4
|
||||
option_bark = 5
|
||||
option_cartoon_fall = 6
|
||||
option_flare_dancer_laugh = 7
|
||||
option_mweep = 8
|
||||
option_shabom_pop = 9
|
||||
option_tambourine = 10
|
||||
|
||||
|
||||
|
||||
@@ -1119,7 +1119,7 @@ hintTable = {
|
||||
'ZD Storms Grotto': ("a small #Fairy Fountain#", None, 'region'),
|
||||
'GF Storms Grotto': ("a small #Fairy Fountain#", None, 'region'),
|
||||
|
||||
'1001': ("Ganondorf 2022!", None, 'junk'),
|
||||
# '1001': ("Ganondorf 2022!", None, 'junk'),
|
||||
'1002': ("They say that monarchy is a terrible system of governance.", None, 'junk'),
|
||||
'1003': ("They say that Zelda is a poor leader.", None, 'junk'),
|
||||
'1004': ("These hints can be quite useful. This is an exception.", None, 'junk'),
|
||||
@@ -1138,12 +1138,12 @@ hintTable = {
|
||||
'1022': ("You're comparing yourself to me?^Ha! You're not even good enough to be my fake.", None, 'junk'),
|
||||
'1023': ("I'll make you eat those words.", None, 'junk'),
|
||||
'1024': ("What happened to Sheik?", None, 'junk'),
|
||||
'1025': ("L2P @.", None, 'junk'),
|
||||
# '1025': ("L2P @.", None, 'junk'),
|
||||
'1026': ("I've heard Sploosh Kaboom is a tricky game.", None, 'junk'),
|
||||
'1027': ("I'm Lonk from Pennsylvania.", None, 'junk'),
|
||||
'1028': ("I bet you'd like to have more bombs.", None, 'junk'),
|
||||
'1029': ("When all else fails, use Fire.", None, 'junk'),
|
||||
'1030': ("Here's a hint, @. Don't be bad.", None, 'junk'),
|
||||
# '1030': ("Here's a hint, @. Don't be bad.", None, 'junk'),
|
||||
'1031': ("Game Over. Return of Ganon.", None, 'junk'),
|
||||
'1032': ("May the way of the Hero lead to the Triforce.", None, 'junk'),
|
||||
'1033': ("Can't find an item? Scan an Amiibo.", None, 'junk'),
|
||||
@@ -1160,7 +1160,7 @@ hintTable = {
|
||||
'1044': ("They say all toasters toast toast.", None, 'junk'),
|
||||
'1045': ("They say that Okami is the best Zelda game.", None, 'junk'),
|
||||
'1046': ("They say that quest guidance can be found at a talking rock.", None, 'junk'),
|
||||
'1047': ("They say that the final item you're looking for can be found somewhere in Hyrule.", None, 'junk'),
|
||||
# '1047': ("They say that the final item you're looking for can be found somewhere in Hyrule.", None, 'junk'),
|
||||
'1048': ("Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.", None, 'junk'),
|
||||
'1049': ("They say that Barinade fears Deku Nuts.", None, 'junk'),
|
||||
'1050': ("They say that Flare Dancers do not fear Goron-crafted blades.", None, 'junk'),
|
||||
@@ -1178,7 +1178,7 @@ hintTable = {
|
||||
'1062': ("Open your eyes.^Open your eyes.^Wake up, @.", None, 'junk'),
|
||||
'1063': ("They say that arbitrary code execution leads to the credits sequence.", None, 'junk'),
|
||||
'1064': ("They say that Twinrova always casts the same spell the first three times.", None, 'junk'),
|
||||
'1065': ("They say that the Development branch may be unstable.", None, 'junk'),
|
||||
# '1065': ("They say that the Development branch may be unstable.", None, 'junk'),
|
||||
'1066': ("You're playing a Randomizer. I'm randomized!^Here's a random number: #4#.&Enjoy your Randomizer!", None, 'junk'),
|
||||
'1067': ("They say Ganondorf's bolts can be reflected with glass or steel.", None, 'junk'),
|
||||
'1068': ("They say Ganon's tail is vulnerable to nuts, arrows, swords, explosives, hammers...^...sticks, seeds, boomerangs...^...rods, shovels, iron balls, angry bees...", None, 'junk'),
|
||||
@@ -1257,7 +1257,7 @@ def hintExclusions(world, clear_cache=False):
|
||||
world.hint_exclusions = []
|
||||
|
||||
for location in world.get_locations():
|
||||
if location.locked or location.excluded:
|
||||
if (location.locked and (location.item.type != 'Song' or world.shuffle_song_items != 'song')) or location.excluded:
|
||||
world.hint_exclusions.append(location.name)
|
||||
|
||||
world_location_names = [
|
||||
|
||||
@@ -650,8 +650,8 @@ def buildWorldGossipHints(world, checkedLocations=None):
|
||||
checkedLocations = {player: set() for player in world.world.player_ids}
|
||||
|
||||
# If Ganondorf can be reached without Light Arrows, add to checkedLocations to prevent extra hinting
|
||||
# Can only be forced with vanilla bridge
|
||||
if world.bridge != 'vanilla':
|
||||
# Can only be forced with vanilla bridge or trials
|
||||
if world.bridge != 'vanilla' and world.trials == 0:
|
||||
try:
|
||||
light_arrow_location = world.world.find_item("Light Arrows", world.player)
|
||||
checkedLocations[light_arrow_location.player].add(light_arrow_location.name)
|
||||
@@ -714,7 +714,6 @@ def buildWorldGossipHints(world, checkedLocations=None):
|
||||
fixed_num = world.hint_dist_user['distribution'][hint_type]['fixed']
|
||||
hint_weight = world.hint_dist_user['distribution'][hint_type]['weight']
|
||||
else:
|
||||
logging.getLogger('').warning("Hint copies is zero for type %s. Assuming this hint type should be disabled.", hint_type)
|
||||
fixed_num = 0
|
||||
hint_weight = 0
|
||||
hint_dist[hint_type] = (hint_weight, world.hint_dist_user['distribution'][hint_type]['copies'])
|
||||
|
||||
@@ -115,13 +115,6 @@ item_difficulty_max = {
|
||||
},
|
||||
}
|
||||
|
||||
TriforceCounts = {
|
||||
'plentiful': Decimal(2.00),
|
||||
'balanced': Decimal(1.50),
|
||||
'scarce': Decimal(1.25),
|
||||
'minimal': Decimal(1.00),
|
||||
}
|
||||
|
||||
DT_vanilla = (
|
||||
['Recovery Heart'] * 2)
|
||||
|
||||
@@ -762,26 +755,22 @@ def generate_itempool(ootworld):
|
||||
|
||||
junk_pool = get_junk_pool(ootworld)
|
||||
|
||||
fixed_locations = list(filter(lambda loc: loc.name in fixedlocations, ootworld.get_locations()))
|
||||
fixed_locations = filter(lambda loc: loc.name in fixedlocations, ootworld.get_locations())
|
||||
for location in fixed_locations:
|
||||
item = fixedlocations[location.name]
|
||||
world.push_item(location, ootworld.create_item(item), collect=False)
|
||||
location.locked = True
|
||||
location.place_locked_item(ootworld.create_item(item))
|
||||
|
||||
drop_locations = list(filter(lambda loc: loc.type == 'Drop', ootworld.get_locations()))
|
||||
drop_locations = filter(lambda loc: loc.type == 'Drop', ootworld.get_locations())
|
||||
for drop_location in drop_locations:
|
||||
item = droplocations[drop_location.name]
|
||||
world.push_item(drop_location, ootworld.create_item(item), collect=False)
|
||||
drop_location.locked = True
|
||||
drop_location.place_locked_item(ootworld.create_item(item))
|
||||
|
||||
# set up item pool
|
||||
(pool, placed_items, skip_in_spoiler_locations) = get_pool_core(ootworld)
|
||||
ootworld.itempool = [ootworld.create_item(item) for item in pool]
|
||||
for (location_name, item) in placed_items.items():
|
||||
location = world.get_location(location_name, player)
|
||||
world.push_item(location, ootworld.create_item(item), collect=False)
|
||||
location.locked = True
|
||||
location.event = True # make sure it's checked during fill
|
||||
location.place_locked_item(ootworld.create_item(item))
|
||||
if location_name in skip_in_spoiler_locations:
|
||||
location.show_in_spoiler = False
|
||||
|
||||
@@ -1360,7 +1349,7 @@ def get_pool_core(world):
|
||||
world.remove_from_start_inventory.append(item.name)
|
||||
|
||||
if world.triforce_hunt:
|
||||
triforce_count = int((TriforceCounts[world.item_pool_value] * world.triforce_goal).to_integral_value(rounding=ROUND_HALF_UP))
|
||||
triforce_count = int((Decimal(100 + world.extra_triforce_percentage)/100 * world.triforce_goal).to_integral_value(rounding=ROUND_HALF_UP))
|
||||
pending_junk_pool.extend(['Triforce Piece'] * triforce_count)
|
||||
|
||||
if world.shuffle_ganon_bosskey == 'on_lacs':
|
||||
@@ -1408,3 +1397,16 @@ def get_pool_core(world):
|
||||
pool.append(pending_item)
|
||||
|
||||
return (pool, placed_items, skip_in_spoiler_locations)
|
||||
|
||||
def add_dungeon_items(ootworld):
|
||||
"""Adds maps, compasses, small keys, boss keys, and Ganon boss key into item pool if they are not placed."""
|
||||
skip_add_settings = {'remove', 'startwith', 'vanilla', 'on_lacs'}
|
||||
for dungeon in ootworld.dungeons:
|
||||
if ootworld.shuffle_mapcompass not in skip_add_settings:
|
||||
ootworld.itempool.extend(dungeon.dungeon_items)
|
||||
if ootworld.shuffle_smallkeys not in skip_add_settings:
|
||||
ootworld.itempool.extend(dungeon.small_keys)
|
||||
if dungeon.name != 'Ganons Castle' and ootworld.shuffle_bosskeys not in skip_add_settings:
|
||||
ootworld.itempool.extend(dungeon.boss_key)
|
||||
if dungeon.name == 'Ganons Castle' and ootworld.shuffle_ganon_bosskey not in skip_add_settings:
|
||||
ootworld.itempool.extend(dungeon.boss_key)
|
||||
|
||||
@@ -22,9 +22,10 @@ def ap_id_to_oot_data(ap_id):
|
||||
class OOTItem(Item):
|
||||
game: str = "Ocarina of Time"
|
||||
|
||||
def __init__(self, name, player, data, event):
|
||||
def __init__(self, name, player, data, event, force_not_advancement):
|
||||
(type, advancement, index, special) = data
|
||||
adv = True if advancement else False # this looks silly but the table uses True, False, and None
|
||||
# "advancement" is True, False or None; some items are not advancement based on settings
|
||||
adv = bool(advancement) and not force_not_advancement
|
||||
super(OOTItem, self).__init__(name, adv, oot_data_to_ap_id(data, event), player)
|
||||
self.type = type
|
||||
self.index = index
|
||||
@@ -32,6 +33,8 @@ class OOTItem(Item):
|
||||
self.looks_like_item = None
|
||||
self.price = special.get('price', None) if special else None
|
||||
self.internal = False
|
||||
if force_not_advancement:
|
||||
self.never_exclude = True
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -274,6 +274,8 @@ ITEM_MESSAGES = {
|
||||
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!",
|
||||
0x9097: "\x08You got an \x05\x41Archipelago item\x05\x40!\x01It seems \x05\x41important\x05\x40!",
|
||||
0x9098: "\x08You got an \x05\x43Archipelago item\x05\x40!\x01Doesn't seem like it's needed.",
|
||||
}
|
||||
|
||||
KEYSANITY_MESSAGES = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import typing
|
||||
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionList
|
||||
from .Colors import *
|
||||
import worlds.oot.Sounds as sfx
|
||||
from .ColorSFXOptions import *
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -109,13 +108,21 @@ class TriforceHunt(Toggle):
|
||||
|
||||
|
||||
class TriforceGoal(Range):
|
||||
"""Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting."""
|
||||
"""Number of Triforce pieces required to complete the game."""
|
||||
displayname = "Required Triforce Pieces"
|
||||
range_start = 1
|
||||
range_end = 50
|
||||
range_end = 100
|
||||
default = 20
|
||||
|
||||
|
||||
class ExtraTriforces(Range):
|
||||
"""Percentage of additional Triforce pieces in the pool, separate from the item pool setting."""
|
||||
displayname = "Percentage of Extra Triforce Pieces"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 50
|
||||
|
||||
|
||||
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"
|
||||
@@ -132,6 +139,7 @@ world_options: typing.Dict[str, type(Option)] = {
|
||||
# "spawn_positions": Toggle,
|
||||
"triforce_hunt": TriforceHunt,
|
||||
"triforce_goal": TriforceGoal,
|
||||
"extra_triforce_percentage": ExtraTriforces,
|
||||
"bombchus_in_logic": LogicalChus,
|
||||
# "mq_dungeons": make_range(0, 12),
|
||||
}
|
||||
@@ -176,7 +184,7 @@ class LacsTokens(Range):
|
||||
displayname = "Tokens Required for LACS"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 100
|
||||
default = 40
|
||||
|
||||
|
||||
lacs_options: typing.Dict[str, type(Option)] = {
|
||||
@@ -217,7 +225,7 @@ class BridgeTokens(Range):
|
||||
displayname = "Tokens Required for Bridge"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 100
|
||||
default = 40
|
||||
|
||||
|
||||
bridge_options: typing.Dict[str, type(Option)] = {
|
||||
@@ -237,17 +245,21 @@ class SongShuffle(Choice):
|
||||
|
||||
|
||||
class ShopShuffle(Choice):
|
||||
"""Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops."""
|
||||
"""Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop;
|
||||
"random_number" randomizes the value for each shop. """
|
||||
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
|
||||
option_off = 0
|
||||
option_fixed_number = 1
|
||||
option_random_number = 2
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class ShopSlots(Range):
|
||||
"""Number of items per shop to be randomized into the main itempool.
|
||||
Only active if Shopsanity is set to "fixed_number." """
|
||||
displayname = "Shuffled Shop Slots"
|
||||
range_start = 0
|
||||
range_end = 4
|
||||
|
||||
|
||||
class TokenShuffle(Choice):
|
||||
@@ -310,6 +322,7 @@ class ShuffleMedigoronCarpet(Toggle):
|
||||
shuffle_options: typing.Dict[str, type(Option)] = {
|
||||
"shuffle_song_items": SongShuffle,
|
||||
"shopsanity": ShopShuffle,
|
||||
"shop_slots": ShopSlots,
|
||||
"tokensanity": TokenShuffle,
|
||||
"shuffle_scrubs": ScrubShuffle,
|
||||
"shuffle_cows": ShuffleCows,
|
||||
@@ -478,6 +491,11 @@ timesavers_options: typing.Dict[str, type(Option)] = {
|
||||
}
|
||||
|
||||
|
||||
class CSMC(Toggle):
|
||||
"""Changes chests containing progression into large chests, and nonprogression into small chests."""
|
||||
displayname = "Chest Size Matches Contents"
|
||||
|
||||
|
||||
class Hints(Choice):
|
||||
"""Gossip Stones can give hints about item locations."""
|
||||
displayname = "Gossip Stones"
|
||||
@@ -501,6 +519,7 @@ class HintDistribution(Choice):
|
||||
option_tournament = 6
|
||||
option_useless = 7
|
||||
option_very_strong = 8
|
||||
option_async = 9
|
||||
|
||||
|
||||
class TextShuffle(Choice):
|
||||
@@ -553,7 +572,7 @@ class RupeeStart(Toggle):
|
||||
|
||||
|
||||
misc_options: typing.Dict[str, type(Option)] = {
|
||||
# "clearer_hints": DefaultOnToggle,
|
||||
"correct_chest_sizes": CSMC,
|
||||
"hints": Hints,
|
||||
"hint_dist": HintDistribution,
|
||||
"text_shuffle": TextShuffle,
|
||||
@@ -631,21 +650,6 @@ itempool_options: typing.Dict[str, type(Option)] = {
|
||||
|
||||
# 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"
|
||||
@@ -700,45 +704,35 @@ cosmetic_options: typing.Dict[str, type(Option)] = {
|
||||
"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),
|
||||
"kokiri_color": kokiri_color,
|
||||
"goron_color": goron_color,
|
||||
"zora_color": zora_color,
|
||||
"silver_gauntlets_color": silver_gauntlets_color,
|
||||
"golden_gauntlets_color": golden_gauntlets_color,
|
||||
"mirror_shield_frame_color": mirror_shield_frame_color,
|
||||
"navi_color_default_inner": navi_color_default_inner,
|
||||
"navi_color_default_outer": navi_color_default_outer,
|
||||
"navi_color_enemy_inner": navi_color_enemy_inner,
|
||||
"navi_color_enemy_outer": navi_color_enemy_outer,
|
||||
"navi_color_npc_inner": navi_color_npc_inner,
|
||||
"navi_color_npc_outer": navi_color_npc_outer,
|
||||
"navi_color_prop_inner": navi_color_prop_inner,
|
||||
"navi_color_prop_outer": navi_color_prop_outer,
|
||||
"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"),
|
||||
"sword_trail_color_inner": sword_trail_color_inner,
|
||||
"sword_trail_color_outer": sword_trail_color_outer,
|
||||
"bombchu_trail_color_inner": bombchu_trail_color_inner,
|
||||
"bombchu_trail_color_outer": bombchu_trail_color_outer,
|
||||
"boomerang_trail_color_inner": boomerang_trail_color_inner,
|
||||
"boomerang_trail_color_outer": boomerang_trail_color_outer,
|
||||
"heart_color": heart_color,
|
||||
"magic_color": magic_color,
|
||||
"a_button_color": a_button_color,
|
||||
"b_button_color": b_button_color,
|
||||
"c_button_color": c_button_color,
|
||||
"start_button_color": start_button_color,
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -751,14 +745,14 @@ class SfxOcarina(Choice):
|
||||
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_navi_overworld": sfx_navi_overworld,
|
||||
"sfx_navi_enemy": sfx_navi_enemy,
|
||||
"sfx_low_hp": sfx_low_hp,
|
||||
"sfx_menu_cursor": sfx_menu_cursor,
|
||||
"sfx_menu_select": sfx_menu_select,
|
||||
"sfx_nightfall": sfx_nightfall,
|
||||
"sfx_horse_neigh": sfx_horse_neigh,
|
||||
"sfx_hover_boots": sfx_hover_boots,
|
||||
"sfx_ocarina": SfxOcarina,
|
||||
}
|
||||
|
||||
|
||||
@@ -1331,10 +1331,10 @@ def patch_rom(world, rom):
|
||||
rom.write_int32(0xAE5E04, 0xAD0F00A4)
|
||||
# requiem of spirit
|
||||
rom.write_int32s(0xAC9ABC, [0x3C010001, 0x00300821])
|
||||
# sun song
|
||||
rom.write_int32(0xE09F68, 0x8C6F00A4)
|
||||
rom.write_int32(0xE09F74, 0x01CFC024)
|
||||
rom.write_int32(0xE09FB0, 0x240F0001)
|
||||
# sun song -- commented for AP to always set the relevant bit in event_chk_inf
|
||||
# rom.write_int32(0xE09F68, 0x8C6F00A4)
|
||||
# rom.write_int32(0xE09F74, 0x01CFC024)
|
||||
# rom.write_int32(0xE09FB0, 0x240F0001)
|
||||
# song of time
|
||||
rom.write_int32(0xDB532C, 0x24050003)
|
||||
|
||||
@@ -1624,10 +1624,15 @@ def patch_rom(world, rom):
|
||||
chest_name = 'Spirit Temple Compass Chest'
|
||||
chest_address = 0x2B6B07C
|
||||
location = world.get_location(chest_name)
|
||||
item = read_rom_item(rom, location.item.index)
|
||||
if item['chest_type'] in (1, 3):
|
||||
rom.write_int16(chest_address + 2, 0x0190) # X pos
|
||||
rom.write_int16(chest_address + 6, 0xFABC) # Z pos
|
||||
if location.item.game == 'Ocarina of Time':
|
||||
item = read_rom_item(rom, location.item.index)
|
||||
if item['chest_type'] in (1, 3):
|
||||
rom.write_int16(chest_address + 2, 0x0190) # X pos
|
||||
rom.write_int16(chest_address + 6, 0xFABC) # Z pos
|
||||
else:
|
||||
if location.item.advancement:
|
||||
rom.write_int16(chest_address + 2, 0x0190) # X pos
|
||||
rom.write_int16(chest_address + 6, 0xFABC) # Z pos
|
||||
|
||||
# Move Silver Gauntlets chest if it is small so it is reachable from Spirit Hover Seam
|
||||
if world.logic_rules != 'glitchless':
|
||||
@@ -1635,10 +1640,15 @@ def patch_rom(world, rom):
|
||||
chest_address_0 = 0x21A02D0 # Address in setup 0
|
||||
chest_address_2 = 0x21A06E4 # Address in setup 2
|
||||
location = world.get_location(chest_name)
|
||||
item = read_rom_item(rom, location.item.index)
|
||||
if item['chest_type'] in (1, 3):
|
||||
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
|
||||
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
|
||||
if location.item.game == 'Ocarina of Time':
|
||||
item = read_rom_item(rom, location.item.index)
|
||||
if item['chest_type'] in (1, 3):
|
||||
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
|
||||
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
|
||||
else:
|
||||
if location.item.advancement:
|
||||
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
|
||||
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
|
||||
|
||||
# give dungeon items the correct messages
|
||||
add_item_messages(messages, shop_items, world)
|
||||
@@ -1808,8 +1818,11 @@ def get_override_entry(location):
|
||||
player_id = location.item.player
|
||||
if location.item.game != 'Ocarina of Time':
|
||||
# This is an AP sendable. It's guaranteed to not be None.
|
||||
item_id = 0x0C # Ocarina of Time item, otherwise unused
|
||||
looks_like_item_id = 0
|
||||
if location.item.advancement:
|
||||
item_id = 0xCB
|
||||
else:
|
||||
item_id = 0xCC
|
||||
else:
|
||||
item_id = location.item.index
|
||||
if None in [scene, default, item_id]:
|
||||
@@ -2057,7 +2070,10 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F
|
||||
else:
|
||||
if location.item.game != "Ocarina of Time":
|
||||
item_display = location.item
|
||||
item_display.index = 0x0C # Ocarina of Time item
|
||||
if location.item.advancement:
|
||||
item_display.index = 0xCB
|
||||
else:
|
||||
item_display.index = 0xCC
|
||||
item_display.special = {}
|
||||
elif location.item.looks_like_item is not None:
|
||||
item_display = location.item.looks_like_item
|
||||
@@ -2125,6 +2141,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F
|
||||
update_message_by_id(messages, shop_item.description_message, description_text, 0x03)
|
||||
update_message_by_id(messages, shop_item.purchase_message, purchase_text, 0x03)
|
||||
|
||||
if any(filter(lambda c: c in location.name, {'5', '6', '7', '8'})):
|
||||
world.current_shop_id += 1
|
||||
|
||||
return shop_objs
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
|
||||
from ..AutoWorld import LogicMixin
|
||||
|
||||
|
||||
@@ -27,10 +27,8 @@ class OOTLogic(LogicMixin):
|
||||
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
|
||||
return mult != 'ohko'
|
||||
|
||||
# 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.
|
||||
@@ -74,6 +72,7 @@ class OOTLogic(LogicMixin):
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
|
||||
|
||||
# Sets extra rules on various specific locations not handled by the rule parser.
|
||||
def set_rules(ootworld):
|
||||
logger = logging.getLogger('')
|
||||
|
||||
@@ -90,44 +89,30 @@ def set_rules(ootworld):
|
||||
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')
|
||||
guarantee_hint = ootworld.parser.parse_rule('guarantee_hint')
|
||||
|
||||
for location in filter(lambda location: location.name in ootworld.shop_prices or 'Deku Scrub' in location.name, ootworld.get_locations()):
|
||||
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')
|
||||
location.price = ootworld.shop_prices[location.name]
|
||||
add_rule(location, create_shop_rule(location, ootworld.parser))
|
||||
|
||||
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 ootworld.dungeon_mq['Forest Temple'] and ootworld.shuffle_bosskeys == 'dungeon' and ootworld.shuffle_smallkeys == 'dungeon' and ootworld.tokensanity == 'off':
|
||||
# First room chest needs to be a small key. Make sure the boss key isn't placed here.
|
||||
location = world.get_location('Forest Temple MQ First Room Chest', player)
|
||||
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.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)
|
||||
if ootworld.shuffle_song_items == 'song' and not ootworld.starting_songs:
|
||||
# Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else.
|
||||
# This is required if map/compass included, or any_dungeon shuffle.
|
||||
location = world.get_location('Sheik in Ice Cavern', player)
|
||||
add_item_rule(location, lambda item: item.player == player and item.type == 'Song')
|
||||
|
||||
# TODO: re-add hints once they are working
|
||||
# if location.type == 'HintStone' and ootworld.hints == 'mask':
|
||||
# location.add_rule(is_child)
|
||||
for name in ootworld.always_hints:
|
||||
add_rule(world.get_location(name, player), guarantee_hint)
|
||||
|
||||
# if location.name in ootworld.always_hints:
|
||||
# location.add_rule(guarantee_hint)
|
||||
# TODO: re-add hints once they are working
|
||||
# if location.type == 'HintStone' and ootworld.hints == 'mask':
|
||||
# location.add_rule(is_child)
|
||||
|
||||
|
||||
def create_shop_rule(location, parser):
|
||||
@@ -157,33 +142,32 @@ 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)
|
||||
for location in filter(lambda location: location.item and location.item.type == 'Shop', ootworld.get_locations()):
|
||||
# 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 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)
|
||||
# Add adult only checks
|
||||
if location.item.name in ['Buy Goron Tunic', 'Buy Zora Tunic']:
|
||||
add_rule(location, ootworld.parser.parse_rule('is_adult', location))
|
||||
|
||||
# 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
|
||||
@@ -193,11 +177,11 @@ def set_entrances_based_rules(ootworld):
|
||||
if ootworld.world.accessibility == 'beatable':
|
||||
return
|
||||
|
||||
all_state = ootworld.state_with_items(ootworld.itempool)
|
||||
all_state = ootworld.world.get_all_state(False)
|
||||
|
||||
for location in ootworld.get_locations():
|
||||
for location in filter(lambda location: location.type == 'Shop', 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):
|
||||
if 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)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from .Location import OOTLocation, LocationFactory, location_name_to_id
|
||||
from .Entrance import OOTEntrance
|
||||
from .EntranceShuffle import shuffle_random_entrances
|
||||
from .Items import OOTItem, item_table, oot_data_to_ap_id
|
||||
from .ItemPool import generate_itempool, get_junk_item, get_junk_pool
|
||||
from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool
|
||||
from .Regions import OOTRegion, TimeOfDay
|
||||
from .Rules import set_rules, set_shop_rules, set_entrances_based_rules
|
||||
from .RuleParser import Rule_AST_Transformer
|
||||
@@ -29,6 +29,7 @@ from Utils import get_options, output_path
|
||||
from BaseClasses import MultiWorld, CollectionState, RegionType
|
||||
from Options import Range, Toggle, OptionList
|
||||
from Fill import fill_restrictive, FillError
|
||||
from worlds.generic.Rules import exclusion_rules
|
||||
from ..AutoWorld import World
|
||||
|
||||
location_id_offset = 67000
|
||||
@@ -46,6 +47,7 @@ class OOTWorld(World):
|
||||
data[2] is not None}
|
||||
location_name_to_id = location_name_to_id
|
||||
remote_items: bool = False
|
||||
remote_start_inventory: bool = False
|
||||
|
||||
data_version = 1
|
||||
|
||||
@@ -169,7 +171,6 @@ class OOTWorld(World):
|
||||
self.mq_dungeons_random = False # this will be a deprecated option later
|
||||
self.ocarina_songs = False # just need to pull in the OcarinaSongs module
|
||||
self.big_poe_count = 1 # disabled due to client-side issues for now
|
||||
self.correct_chest_sizes = False # will probably never be implemented since multiworld items are always major
|
||||
# ER options
|
||||
self.shuffle_interior_entrances = 'off'
|
||||
self.shuffle_grotto_entrances = False
|
||||
@@ -180,8 +181,7 @@ class OOTWorld(World):
|
||||
self.spawn_positions = False
|
||||
|
||||
# Set internal names used by the OoT generator
|
||||
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon',
|
||||
'overworld'] # only 'keysanity' and 'remove' implemented
|
||||
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
|
||||
|
||||
# Hint stuff
|
||||
self.misc_hints = True # this is just always on
|
||||
@@ -198,9 +198,14 @@ class OOTWorld(World):
|
||||
self.disable_trade_revert = (self.shuffle_interior_entrances != 'off') or self.shuffle_overworld_entrances
|
||||
self.shuffle_special_interior_entrances = self.shuffle_interior_entrances == 'all'
|
||||
|
||||
# Convert the double option used by shopsanity into a single option
|
||||
if self.shopsanity == 'random_number':
|
||||
self.shopsanity = 'random'
|
||||
elif self.shopsanity == 'fixed_number':
|
||||
self.shopsanity = str(self.shop_slots)
|
||||
|
||||
# fixing some options
|
||||
self.starting_tod = self.starting_tod.replace('_', '-') # Fixes starting time spelling: "witching_hour" -> "witching-hour"
|
||||
self.shopsanity = self.shopsanity.replace('_value', '') # can't set "random" manually
|
||||
self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
|
||||
|
||||
# Get hint distribution
|
||||
@@ -239,6 +244,25 @@ class OOTWorld(World):
|
||||
|
||||
self.always_hints = [hint.name for hint in getRequiredHints(self)]
|
||||
|
||||
# Determine items which are not considered advancement based on settings. They will never be excluded.
|
||||
self.nonadvancement_items = {'Double Defense', 'Ice Arrows'}
|
||||
if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and
|
||||
self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances):
|
||||
# nayru's love may be required to prevent forced damage
|
||||
self.nonadvancement_items.add('Nayrus Love')
|
||||
if getattr(self, 'logic_grottos_without_agony', False) and self.hints != 'agony':
|
||||
# Stone of Agony skippable if not used for hints or grottos
|
||||
self.nonadvancement_items.add('Stone of Agony')
|
||||
if (not self.shuffle_special_interior_entrances and not self.shuffle_overworld_entrances and
|
||||
not self.warp_songs and not self.spawn_positions):
|
||||
# Serenade and Prelude are never required unless one of those settings is enabled
|
||||
self.nonadvancement_items.add('Serenade of Water')
|
||||
self.nonadvancement_items.add('Prelude of Light')
|
||||
if self.logic_rules == 'glitchless':
|
||||
# Both two-handed swords can be required in glitch logic, so only consider them nonprogression in glitchless
|
||||
self.nonadvancement_items.add('Biggoron Sword')
|
||||
self.nonadvancement_items.add('Giants Knife')
|
||||
|
||||
def load_regions_from_json(self, file_path):
|
||||
region_json = read_json(file_path)
|
||||
|
||||
@@ -370,10 +394,8 @@ class OOTWorld(World):
|
||||
boss_locations = [self.world.get_location(loc, self.player) for loc in boss_location_names]
|
||||
|
||||
placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None]
|
||||
unplaced_prizes = [item for item in boss_rewards if item.name not in placed_prizes]
|
||||
empty_boss_locations = [loc for loc in boss_locations if loc.item is None]
|
||||
prizepool = list(unplaced_prizes)
|
||||
prize_locs = list(empty_boss_locations)
|
||||
prizepool = [item for item in boss_rewards if item.name not in placed_prizes]
|
||||
prize_locs = [loc for loc in boss_locations if loc.item is None]
|
||||
|
||||
while bossCount:
|
||||
bossCount -= 1
|
||||
@@ -387,8 +409,8 @@ class OOTWorld(World):
|
||||
|
||||
def create_item(self, name: str):
|
||||
if name in item_table:
|
||||
return OOTItem(name, self.player, item_table[name], False)
|
||||
return OOTItem(name, self.player, ('Event', True, None, None), True)
|
||||
return OOTItem(name, self.player, item_table[name], False, (name in self.nonadvancement_items))
|
||||
return OOTItem(name, self.player, ('Event', True, None, None), True, False)
|
||||
|
||||
def make_event_item(self, name, location, item=None):
|
||||
if item is None:
|
||||
@@ -428,19 +450,19 @@ class OOTWorld(World):
|
||||
if self.entrance_shuffle:
|
||||
shuffle_random_entrances(self)
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self)
|
||||
|
||||
def generate_basic(self): # generate item pools, place fixed items
|
||||
def create_items(self):
|
||||
# Generate itempool
|
||||
generate_itempool(self)
|
||||
add_dungeon_items(self)
|
||||
junk_pool = get_junk_pool(self)
|
||||
removed_items = []
|
||||
# Determine starting items
|
||||
for item in self.world.precollected_items:
|
||||
if item.player != self.player:
|
||||
continue
|
||||
if item.name in self.remove_from_start_inventory:
|
||||
self.remove_from_start_inventory.remove(item.name)
|
||||
removed_items.append(item.name)
|
||||
else:
|
||||
self.starting_items[item.name] += 1
|
||||
if item.type == 'Song':
|
||||
@@ -455,164 +477,33 @@ class OOTWorld(World):
|
||||
if self.start_with_rupees:
|
||||
self.starting_items['Rupees'] = 999
|
||||
|
||||
# Uniquely rename drop locations for each region and erase them from the spoiler
|
||||
set_drop_location_names(self)
|
||||
self.world.itempool += self.itempool
|
||||
self.remove_from_start_inventory.extend(removed_items)
|
||||
|
||||
# Fill boss prizes
|
||||
self.fill_bosses()
|
||||
|
||||
# relevant for both dungeon item fill and song fill
|
||||
dungeon_song_locations = [
|
||||
"Deku Tree Queen Gohma Heart",
|
||||
"Dodongos Cavern King Dodongo Heart",
|
||||
"Jabu Jabus Belly Barinade Heart",
|
||||
"Forest Temple Phantom Ganon Heart",
|
||||
"Fire Temple Volvagia Heart",
|
||||
"Water Temple Morpha Heart",
|
||||
"Shadow Temple Bongo Bongo Heart",
|
||||
"Spirit Temple Twinrova Heart",
|
||||
"Song from Impa",
|
||||
"Sheik in Ice Cavern",
|
||||
"Bottom of the Well Lens of Truth Chest", "Bottom of the Well MQ Lens of Truth Chest", # only one exists
|
||||
"Gerudo Training Grounds Maze Path Final Chest", "Gerudo Training Grounds MQ Ice Arrows Chest", # only one exists
|
||||
]
|
||||
|
||||
# Place/set rules for dungeon items
|
||||
itempools = {
|
||||
'dungeon': [],
|
||||
'overworld': [],
|
||||
'any_dungeon': [],
|
||||
'keysanity': [],
|
||||
}
|
||||
any_dungeon_locations = []
|
||||
for dungeon in self.dungeons:
|
||||
itempools['dungeon'] = []
|
||||
# Put the dungeon items into their appropriate pools.
|
||||
# Build in reverse order since we need to fill boss key first and pop() returns the last element
|
||||
if self.shuffle_mapcompass in itempools:
|
||||
itempools[self.shuffle_mapcompass].extend(dungeon.dungeon_items)
|
||||
if self.shuffle_smallkeys in itempools:
|
||||
itempools[self.shuffle_smallkeys].extend(dungeon.small_keys)
|
||||
shufflebk = self.shuffle_bosskeys if dungeon.name != 'Ganons Castle' else self.shuffle_ganon_bosskey
|
||||
if shufflebk in itempools:
|
||||
itempools[shufflebk].extend(dungeon.boss_key)
|
||||
|
||||
# We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it.
|
||||
dungeon_locations = [loc for region in dungeon.regions for loc in region.locations
|
||||
if loc.item is None and (
|
||||
self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)]
|
||||
if itempools['dungeon']: # only do this if there's anything to shuffle
|
||||
self.world.random.shuffle(dungeon_locations)
|
||||
fill_restrictive(self.world, self.state_with_items(self.itempool), dungeon_locations,
|
||||
itempools['dungeon'], True, True)
|
||||
any_dungeon_locations.extend(dungeon_locations) # adds only the unfilled locations
|
||||
|
||||
# Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary
|
||||
if self.shuffle_fortresskeys == 'any_dungeon':
|
||||
fortresskeys = list(
|
||||
filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.itempool))
|
||||
itempools['any_dungeon'].extend(fortresskeys)
|
||||
for key in fortresskeys:
|
||||
self.itempool.remove(key)
|
||||
if itempools['any_dungeon']:
|
||||
itempools['any_dungeon'].sort(
|
||||
key=lambda item: {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type,
|
||||
0))
|
||||
self.world.random.shuffle(any_dungeon_locations)
|
||||
fill_restrictive(self.world, self.state_with_items(self.itempool), any_dungeon_locations,
|
||||
itempools['any_dungeon'], True, True)
|
||||
|
||||
# If anything is overworld-only, enforce them as local and not in the remaining dungeon locations
|
||||
if itempools['overworld'] or self.shuffle_fortresskeys == 'overworld':
|
||||
from worlds.generic.Rules import forbid_items_for_player
|
||||
fortresskeys = {'Small Key (Gerudo Fortress)'} if self.shuffle_fortresskeys == 'overworld' else set()
|
||||
local_overworld_items = set(map(lambda item: item.name, itempools['overworld'])).union(fortresskeys)
|
||||
for location in self.world.get_locations():
|
||||
if location.player != self.player or location in any_dungeon_locations:
|
||||
forbid_items_for_player(location, local_overworld_items, self.player)
|
||||
self.itempool.extend(itempools['overworld'])
|
||||
|
||||
# Dump keysanity items into the itempool
|
||||
self.itempool.extend(itempools['keysanity'])
|
||||
|
||||
# Now that keys are in the pool, we can forbid tunics from child-only shops
|
||||
def set_rules(self):
|
||||
set_rules(self)
|
||||
set_entrances_based_rules(self)
|
||||
|
||||
# Place songs
|
||||
# 5 built-in retries because this section can fail sometimes
|
||||
if self.shuffle_song_items != 'any':
|
||||
tries = 5
|
||||
if self.shuffle_song_items == 'song':
|
||||
song_locations = list(filter(lambda location: location.type == 'Song',
|
||||
self.world.get_unfilled_locations(player=self.player)))
|
||||
elif self.shuffle_song_items == 'dungeon':
|
||||
song_locations = list(filter(lambda location: location.name in dungeon_song_locations,
|
||||
self.world.get_unfilled_locations(player=self.player)))
|
||||
else:
|
||||
raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}")
|
||||
def generate_basic(self): # mostly killing locations that shouldn't exist by settings
|
||||
|
||||
songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.itempool))
|
||||
for song in songs:
|
||||
self.itempool.remove(song)
|
||||
while tries:
|
||||
try:
|
||||
self.world.random.shuffle(songs) # shuffling songs makes it less likely to fail by placing ZL last
|
||||
self.world.random.shuffle(song_locations)
|
||||
fill_restrictive(self.world, self.state_with_items(self.itempool), song_locations[:], songs[:],
|
||||
True, True)
|
||||
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
|
||||
tries = 0
|
||||
except FillError as e:
|
||||
tries -= 1
|
||||
if tries == 0:
|
||||
raise e
|
||||
logger.debug(f"Failed placing songs for player {self.player}. Retries left: {tries}")
|
||||
# undo what was done
|
||||
for song in songs:
|
||||
song.location = None
|
||||
song.world = None
|
||||
for location in song_locations:
|
||||
location.item = None
|
||||
location.locked = False
|
||||
location.event = False
|
||||
# Fill boss prizes. needs to happen before killing unreachable locations
|
||||
self.fill_bosses()
|
||||
|
||||
# Place shop items
|
||||
# fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
|
||||
if self.shopsanity != 'off':
|
||||
shop_items = list(filter(lambda item: item.player == self.player and item.type == 'Shop', self.itempool))
|
||||
shop_locations = list(
|
||||
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
|
||||
self.world.get_unfilled_locations(player=self.player)))
|
||||
shop_items.sort(key=lambda item: 1 if item.name in ["Buy Goron Tunic", "Buy Zora Tunic"] else 0)
|
||||
self.world.random.shuffle(shop_locations)
|
||||
for item in shop_items:
|
||||
self.itempool.remove(item)
|
||||
fill_restrictive(self.world, self.state_with_items(self.itempool), shop_locations, shop_items, True, True)
|
||||
set_shop_rules(self)
|
||||
|
||||
# Locations which are not sendable must be converted to events
|
||||
# This includes all locations for which show_in_spoiler is false, and shuffled shop items.
|
||||
for loc in self.get_locations():
|
||||
if loc.address is not None and (
|
||||
not loc.show_in_spoiler or (loc.item is not None and loc.item.type == 'Shop')
|
||||
or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
|
||||
loc.address = None
|
||||
# Uniquely rename drop locations for each region and erase them from the spoiler
|
||||
set_drop_location_names(self)
|
||||
|
||||
# Gather items for ice trap appearances
|
||||
self.fake_items = []
|
||||
if self.ice_trap_appearance in ['major_only', 'anything']:
|
||||
self.fake_items.extend([item for item in self.itempool if item.index and self.is_major_item(item)])
|
||||
self.fake_items.extend(item for item in self.itempool if item.index and self.is_major_item(item))
|
||||
if self.ice_trap_appearance in ['junk_only', 'anything']:
|
||||
self.fake_items.extend([item for item in self.itempool if
|
||||
item.index and not self.is_major_item(item) and item.name != 'Ice Trap'])
|
||||
|
||||
# Put all remaining items into the general itempool
|
||||
self.world.itempool += self.itempool
|
||||
self.fake_items.extend(item for item in self.itempool if
|
||||
item.index and not self.is_major_item(item) and item.name != 'Ice Trap')
|
||||
|
||||
# Kill unreachable events that can't be gotten even with all items
|
||||
# Make sure to only kill actual internal events, not in-game "events"
|
||||
all_state = self.state_with_items(self.itempool)
|
||||
all_locations = [loc for loc in self.world.get_locations() if loc.player == self.player]
|
||||
all_state = self.world.get_all_state(False)
|
||||
all_locations = self.get_locations()
|
||||
reachable = self.world.get_reachable_locations(all_state, self.player)
|
||||
unreachable = [loc for loc in all_locations if
|
||||
loc.internal and loc.event and loc.locked and loc not in reachable]
|
||||
@@ -635,17 +526,171 @@ class OOTWorld(World):
|
||||
loc.parent_region.locations.remove(loc)
|
||||
|
||||
def pre_fill(self):
|
||||
|
||||
# relevant for both dungeon item fill and song fill
|
||||
dungeon_song_locations = [
|
||||
"Deku Tree Queen Gohma Heart",
|
||||
"Dodongos Cavern King Dodongo Heart",
|
||||
"Jabu Jabus Belly Barinade Heart",
|
||||
"Forest Temple Phantom Ganon Heart",
|
||||
"Fire Temple Volvagia Heart",
|
||||
"Water Temple Morpha Heart",
|
||||
"Shadow Temple Bongo Bongo Heart",
|
||||
"Spirit Temple Twinrova Heart",
|
||||
"Song from Impa",
|
||||
"Sheik in Ice Cavern",
|
||||
"Bottom of the Well Lens of Truth Chest", "Bottom of the Well MQ Lens of Truth Chest", # only one exists
|
||||
"Gerudo Training Grounds Maze Path Final Chest", "Gerudo Training Grounds MQ Ice Arrows Chest", # only one exists
|
||||
]
|
||||
|
||||
# Place/set rules for dungeon items
|
||||
itempools = {
|
||||
'dungeon': [],
|
||||
'overworld': [],
|
||||
'any_dungeon': [],
|
||||
}
|
||||
any_dungeon_locations = []
|
||||
for dungeon in self.dungeons:
|
||||
itempools['dungeon'] = []
|
||||
# Put the dungeon items into their appropriate pools.
|
||||
# Build in reverse order since we need to fill boss key first and pop() returns the last element
|
||||
if self.shuffle_mapcompass in itempools:
|
||||
itempools[self.shuffle_mapcompass].extend(dungeon.dungeon_items)
|
||||
if self.shuffle_smallkeys in itempools:
|
||||
itempools[self.shuffle_smallkeys].extend(dungeon.small_keys)
|
||||
shufflebk = self.shuffle_bosskeys if dungeon.name != 'Ganons Castle' else self.shuffle_ganon_bosskey
|
||||
if shufflebk in itempools:
|
||||
itempools[shufflebk].extend(dungeon.boss_key)
|
||||
|
||||
# We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it.
|
||||
dungeon_locations = [loc for region in dungeon.regions for loc in region.locations
|
||||
if loc.item is None and (
|
||||
self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)]
|
||||
if itempools['dungeon']: # only do this if there's anything to shuffle
|
||||
for item in itempools['dungeon']:
|
||||
self.world.itempool.remove(item)
|
||||
self.world.random.shuffle(dungeon_locations)
|
||||
fill_restrictive(self.world, self.world.get_all_state(False), dungeon_locations,
|
||||
itempools['dungeon'], True, True)
|
||||
any_dungeon_locations.extend(dungeon_locations) # adds only the unfilled locations
|
||||
|
||||
# Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary
|
||||
if self.shuffle_fortresskeys == 'any_dungeon':
|
||||
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.world.itempool)
|
||||
itempools['any_dungeon'].extend(fortresskeys)
|
||||
if itempools['any_dungeon']:
|
||||
for item in itempools['any_dungeon']:
|
||||
self.world.itempool.remove(item)
|
||||
itempools['any_dungeon'].sort(key=lambda item:
|
||||
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
|
||||
self.world.random.shuffle(any_dungeon_locations)
|
||||
fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations,
|
||||
itempools['any_dungeon'], True, True)
|
||||
|
||||
# If anything is overworld-only, enforce them as local and not in the remaining dungeon locations
|
||||
if itempools['overworld'] or self.shuffle_fortresskeys == 'overworld':
|
||||
from worlds.generic.Rules import forbid_items_for_player
|
||||
fortresskeys = {'Small Key (Gerudo Fortress)'} if self.shuffle_fortresskeys == 'overworld' else set()
|
||||
local_overworld_items = set(map(lambda item: item.name, itempools['overworld'])).union(fortresskeys)
|
||||
for location in self.world.get_locations():
|
||||
if location.player != self.player or location in any_dungeon_locations:
|
||||
forbid_items_for_player(location, local_overworld_items, self.player)
|
||||
|
||||
# Place songs
|
||||
# 5 built-in retries because this section can fail sometimes
|
||||
if self.shuffle_song_items != 'any':
|
||||
tries = 5
|
||||
if self.shuffle_song_items == 'song':
|
||||
song_locations = list(filter(lambda location: location.type == 'Song',
|
||||
self.world.get_unfilled_locations(player=self.player)))
|
||||
elif self.shuffle_song_items == 'dungeon':
|
||||
song_locations = list(filter(lambda location: location.name in dungeon_song_locations,
|
||||
self.world.get_unfilled_locations(player=self.player)))
|
||||
else:
|
||||
raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}")
|
||||
|
||||
songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.world.itempool))
|
||||
for song in songs:
|
||||
self.world.itempool.remove(song)
|
||||
while tries:
|
||||
try:
|
||||
self.world.random.shuffle(songs) # shuffling songs makes it less likely to fail by placing ZL last
|
||||
self.world.random.shuffle(song_locations)
|
||||
fill_restrictive(self.world, self.world.get_all_state(False), song_locations[:], songs[:],
|
||||
True, True)
|
||||
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
|
||||
tries = 0
|
||||
except FillError as e:
|
||||
tries -= 1
|
||||
if tries == 0:
|
||||
raise e
|
||||
logger.debug(f"Failed placing songs for player {self.player}. Retries left: {tries}")
|
||||
# undo what was done
|
||||
for song in songs:
|
||||
song.location = None
|
||||
song.world = None
|
||||
for location in song_locations:
|
||||
location.item = None
|
||||
location.locked = False
|
||||
location.event = False
|
||||
|
||||
# Place shop items
|
||||
# fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
|
||||
if self.shopsanity != 'off':
|
||||
shop_items = list(filter(lambda item: item.player == self.player and item.type == 'Shop', self.world.itempool))
|
||||
shop_locations = list(
|
||||
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
|
||||
self.world.get_unfilled_locations(player=self.player)))
|
||||
shop_items.sort(key=lambda item: 1 if item.name in {"Buy Goron Tunic", "Buy Zora Tunic"} else 0)
|
||||
self.world.random.shuffle(shop_locations)
|
||||
for item in shop_items:
|
||||
self.world.itempool.remove(item)
|
||||
fill_restrictive(self.world, self.world.get_all_state(False), shop_locations, shop_items, True, True)
|
||||
set_shop_rules(self) # sets wallet requirements on shop items, must be done after they are filled
|
||||
|
||||
# If skip child zelda is active and Song from Impa is unfilled, put a local giveable item into it.
|
||||
impa = self.world.get_location("Song from Impa", self.player)
|
||||
if self.skip_child_zelda and impa.item is None:
|
||||
from .SaveContext import SaveContext
|
||||
item_to_place = self.world.random.choice([item for item in self.world.itempool
|
||||
if
|
||||
item.player == self.player and item.name in SaveContext.giveable_items])
|
||||
self.world.push_item(impa, item_to_place, False)
|
||||
impa.locked = True
|
||||
impa.event = True
|
||||
self.world.itempool.remove(item_to_place)
|
||||
if self.skip_child_zelda:
|
||||
if impa.item is None:
|
||||
from .SaveContext import SaveContext
|
||||
item_to_place = self.world.random.choice(list(item for item in self.world.itempool if
|
||||
item.player == self.player and item.name in SaveContext.giveable_items))
|
||||
impa.place_locked_item(item_to_place)
|
||||
self.world.itempool.remove(item_to_place)
|
||||
# Give items to startinventory
|
||||
self.world.push_precollected(impa.item)
|
||||
self.world.push_precollected(self.create_item("Zeldas Letter"))
|
||||
|
||||
# Exclude locations in Ganon's Castle proportional to the number of items required to make the bridge
|
||||
# Check for dungeon ER later
|
||||
if self.logic_rules == 'glitchless':
|
||||
if self.bridge == 'medallions':
|
||||
ganon_junk_fill = self.bridge_medallions / 9
|
||||
elif self.bridge == 'stones':
|
||||
ganon_junk_fill = self.bridge_stones / 9
|
||||
elif self.bridge == 'dungeons':
|
||||
ganon_junk_fill = self.bridge_rewards / 9
|
||||
elif self.bridge == 'vanilla':
|
||||
ganon_junk_fill = 2 / 9
|
||||
elif self.bridge == 'tokens':
|
||||
ganon_junk_fill = self.bridge_tokens / 100
|
||||
elif self.bridge == 'open':
|
||||
ganon_junk_fill = 0
|
||||
else:
|
||||
raise Exception("Unexpected bridge setting")
|
||||
|
||||
gc = next(filter(lambda dungeon: dungeon.name == 'Ganons Castle', self.dungeons))
|
||||
locations = [loc.name for region in gc.regions for loc in region.locations if loc.item is None]
|
||||
junk_fill_locations = self.world.random.sample(locations, round(len(locations) * ganon_junk_fill))
|
||||
exclusion_rules(self.world, self.player, junk_fill_locations)
|
||||
|
||||
# Locations which are not sendable must be converted to events
|
||||
# This includes all locations for which show_in_spoiler is false, and shuffled shop items.
|
||||
for loc in self.get_locations():
|
||||
if loc.address is not None and (
|
||||
not loc.show_in_spoiler or (loc.item is not None and loc.item.type == 'Shop')
|
||||
or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
|
||||
loc.address = None
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
if self.hints != 'none':
|
||||
@@ -668,53 +713,85 @@ class OOTWorld(World):
|
||||
rom.restore()
|
||||
|
||||
# Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations.
|
||||
def stage_generate_output(world: MultiWorld, output_directory: str):
|
||||
try:
|
||||
items_by_region = {player: {} for player in world.get_game_players("Ocarina of Time") if world.worlds[player].hints != 'none'}
|
||||
if items_by_region:
|
||||
for player in items_by_region:
|
||||
for r in world.worlds[player].regions:
|
||||
items_by_region[player][r.hint_text] = {'dungeon': False, 'weight': 0, 'prog_items': 0}
|
||||
for d in world.worlds[player].dungeons:
|
||||
items_by_region[player][d.hint_text] = {'dungeon': True, 'weight': 0, 'prog_items': 0}
|
||||
del (items_by_region[player]["Link's Pocket"])
|
||||
del (items_by_region[player][None])
|
||||
@classmethod
|
||||
def stage_generate_output(cls, world: MultiWorld, output_directory: str):
|
||||
def hint_type_players(hint_type: str) -> set:
|
||||
return {autoworld.player for autoworld in world.get_game_worlds("Ocarina of Time")
|
||||
if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0}
|
||||
|
||||
try:
|
||||
item_hint_players = hint_type_players('item')
|
||||
barren_hint_players = hint_type_players('barren')
|
||||
woth_hint_players = hint_type_players('woth')
|
||||
|
||||
items_by_region = {}
|
||||
for player in barren_hint_players:
|
||||
items_by_region[player] = {}
|
||||
for r in world.worlds[player].regions:
|
||||
items_by_region[player][r.hint_text] = {'dungeon': False, 'weight': 0, 'is_barren': True}
|
||||
for d in world.worlds[player].dungeons:
|
||||
items_by_region[player][d.hint_text] = {'dungeon': True, 'weight': 0, 'is_barren': True}
|
||||
del (items_by_region[player]["Link's Pocket"])
|
||||
del (items_by_region[player][None])
|
||||
|
||||
if item_hint_players: # loop once over all locations to gather major items. Check oot locations for barren/woth if needed
|
||||
for loc in world.get_locations():
|
||||
player = loc.item.player
|
||||
autoworld = world.worlds[player]
|
||||
if ((player in items_by_region and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item']))
|
||||
or (loc.player in items_by_region and loc.name in world.worlds[loc.player].added_hint_types['item'])):
|
||||
if ((player in item_hint_players and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item']))
|
||||
or (loc.player in item_hint_players and loc.name in world.worlds[loc.player].added_hint_types['item'])):
|
||||
autoworld.major_item_locations.append(loc)
|
||||
|
||||
if loc.game == "Ocarina of Time":
|
||||
if loc.item.code and (not loc.locked or loc.item.type == 'Song'): # shuffled item
|
||||
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or loc.item.type == 'Song'):
|
||||
if loc.player in barren_hint_players:
|
||||
hint_area = get_hint_area(loc)
|
||||
items_by_region[loc.player][hint_area]['weight'] += 1
|
||||
if loc.item.advancement:
|
||||
# Non-locked progression. Increment counter
|
||||
items_by_region[loc.player][hint_area]['prog_items'] += 1
|
||||
# Skip item at location and see if game is still beatable
|
||||
items_by_region[loc.player][hint_area]['is_barren'] = False
|
||||
if loc.player in woth_hint_players and loc.item.advancement:
|
||||
# Skip item at location and see if game is still beatable
|
||||
state = CollectionState(world)
|
||||
state.locations_checked.add(loc)
|
||||
if not world.can_beat_game(state):
|
||||
world.worlds[loc.player].required_locations.append(loc)
|
||||
elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth
|
||||
for player in (barren_hint_players | woth_hint_players):
|
||||
for loc in world.worlds[player].get_locations():
|
||||
if loc.item.code and (not loc.locked or loc.item.type == 'Song'):
|
||||
if player in barren_hint_players:
|
||||
hint_area = get_hint_area(loc)
|
||||
items_by_region[player][hint_area]['weight'] += 1
|
||||
if loc.item.advancement:
|
||||
items_by_region[player][hint_area]['is_barren'] = False
|
||||
if player in woth_hint_players and loc.item.advancement:
|
||||
state = CollectionState(world)
|
||||
state.locations_checked.add(loc)
|
||||
if not world.can_beat_game(state):
|
||||
world.worlds[loc.player].required_locations.append(loc)
|
||||
|
||||
for autoworld in world.get_game_worlds("Ocarina of Time"):
|
||||
autoworld.empty_areas = {region: info for (region, info) in items_by_region[autoworld.player].items() if not info['prog_items']}
|
||||
world.worlds[player].required_locations.append(loc)
|
||||
for player in barren_hint_players:
|
||||
world.worlds[player].empty_areas = {region: info for (region, info) in items_by_region[player].items() if info['is_barren']}
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
hint_data_available.set()
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
for item_name in self.remove_from_start_inventory:
|
||||
item_id = self.item_name_to_id.get(item_name, None)
|
||||
try:
|
||||
multidata["precollected_items"][self.player].remove(item_id)
|
||||
except ValueError as e:
|
||||
logger.warning(f"Attempted to remove nonexistent item id {item_id} from OoT precollected items ({item_name})")
|
||||
|
||||
|
||||
# Helper functions
|
||||
def get_shuffled_entrances(self):
|
||||
return []
|
||||
return [] # later this will return all entrances modified by ER. patching process needs it now though
|
||||
|
||||
# make this a generator later?
|
||||
def get_locations(self):
|
||||
return [loc for region in self.regions for loc in region.locations]
|
||||
for region in self.regions:
|
||||
for loc in region.locations:
|
||||
yield loc
|
||||
|
||||
def get_location(self, location):
|
||||
return self.world.get_location(location, self.player)
|
||||
@@ -722,17 +799,13 @@ class OOTWorld(World):
|
||||
def get_region(self, region):
|
||||
return self.world.get_region(region, self.player)
|
||||
|
||||
def state_with_items(self, items):
|
||||
ret = CollectionState(self.world)
|
||||
for item in items:
|
||||
self.collect(ret, item)
|
||||
ret.sweep_for_events()
|
||||
return ret
|
||||
|
||||
def is_major_item(self, item: OOTItem):
|
||||
if item.type == 'Token':
|
||||
return self.bridge == 'tokens' or self.lacs_condition == 'tokens'
|
||||
|
||||
if item.name in self.nonadvancement_items:
|
||||
return True
|
||||
|
||||
if item.type in ('Drop', 'Event', 'Shop', 'DungeonReward') or not item.advancement:
|
||||
return False
|
||||
|
||||
|
||||
81
worlds/oot/build_color_options.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# Quick script to build top-level color and sfx options for pickling
|
||||
|
||||
from Colors import *
|
||||
import Sounds as sfx
|
||||
|
||||
|
||||
def assemble_color_option(f, internal_name: str, 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)}
|
||||
|
||||
docstring = 'Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.'
|
||||
if outer:
|
||||
docstring += ' "match_inner" copies the inner color for this option.'
|
||||
|
||||
f.write(f"class {internal_name}(Choice):\n")
|
||||
f.write(f" \"\"\"{docstring}\"\"\"\n")
|
||||
f.write(f" displayname = \"{display_name}\"\n")
|
||||
for color, id in color_to_id.items():
|
||||
f.write(f" option_{color} = {id}\n")
|
||||
f.write(f" default = {color_options.index(default_option)}")
|
||||
f.write(f"\n\n\n")
|
||||
|
||||
|
||||
def assemble_sfx_option(f, internal_name: str, sound_hook: sfx.SoundHooks, display_name: str):
|
||||
options = sfx.get_setting_choices(sound_hook).keys()
|
||||
sfx_to_id = {sound.replace('-', '_'): index for index, sound in enumerate(options)}
|
||||
docstring = 'Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.'
|
||||
|
||||
f.write(f"class {internal_name}(Choice):\n")
|
||||
f.write(f" \"\"\"{docstring}\"\"\"\n")
|
||||
f.write(f" displayname = \"{display_name}\"\n")
|
||||
for sound, id in sfx_to_id.items():
|
||||
f.write(f" option_{sound} = {id}\n")
|
||||
f.write(f"\n\n\n")
|
||||
|
||||
|
||||
with open('ColorSFXOptions.py', 'w') as f:
|
||||
|
||||
f.write("# Auto-generated color and sound-effect options from Colors.py and Sounds.py \n")
|
||||
f.write("from Options import Choice\n\n\n")
|
||||
|
||||
assemble_color_option(f, "kokiri_color", get_tunic_color_options, "Kokiri Tunic", "Kokiri Green")
|
||||
assemble_color_option(f, "goron_color", get_tunic_color_options, "Goron Tunic", "Goron Red")
|
||||
assemble_color_option(f, "zora_color", get_tunic_color_options, "Zora Tunic", "Zora Blue")
|
||||
assemble_color_option(f, "silver_gauntlets_color", get_gauntlet_color_options, "Silver Gauntlets Color", "Silver")
|
||||
assemble_color_option(f, "golden_gauntlets_color", get_gauntlet_color_options, "Golden Gauntlets Color", "Gold")
|
||||
assemble_color_option(f, "mirror_shield_frame_color", get_shield_frame_color_options, "Mirror Shield Frame Color", "Red")
|
||||
assemble_color_option(f, "navi_color_default_inner", get_navi_color_options, "Navi Idle Inner", "White")
|
||||
assemble_color_option(f, "navi_color_default_outer", get_navi_color_options, "Navi Idle Outer", "Match Inner", outer=True)
|
||||
assemble_color_option(f, "navi_color_enemy_inner", get_navi_color_options, "Navi Targeting Enemy Inner", "Yellow")
|
||||
assemble_color_option(f, "navi_color_enemy_outer", get_navi_color_options, "Navi Targeting Enemy Outer", "Match Inner", outer=True)
|
||||
assemble_color_option(f, "navi_color_npc_inner", get_navi_color_options, "Navi Targeting NPC Inner", "Light Blue")
|
||||
assemble_color_option(f, "navi_color_npc_outer", get_navi_color_options, "Navi Targeting NPC Outer", "Match Inner", outer=True)
|
||||
assemble_color_option(f, "navi_color_prop_inner", get_navi_color_options, "Navi Targeting Prop Inner", "Green")
|
||||
assemble_color_option(f, "navi_color_prop_outer", get_navi_color_options, "Navi Targeting Prop Outer", "Match Inner", outer=True)
|
||||
assemble_color_option(f, "sword_trail_color_inner", get_sword_trail_color_options, "Sword Trail Inner", "White")
|
||||
assemble_color_option(f, "sword_trail_color_outer", get_sword_trail_color_options, "Sword Trail Outer", "Match Inner", outer=True)
|
||||
assemble_color_option(f, "bombchu_trail_color_inner", get_bombchu_trail_color_options, "Bombchu Trail Inner", "Red")
|
||||
assemble_color_option(f, "bombchu_trail_color_outer", get_bombchu_trail_color_options, "Bombchu Trail Outer", "Match Inner", outer=True)
|
||||
assemble_color_option(f, "boomerang_trail_color_inner", get_boomerang_trail_color_options, "Boomerang Trail Inner", "Yellow")
|
||||
assemble_color_option(f, "boomerang_trail_color_outer", get_boomerang_trail_color_options, "Boomerang Trail Outer", "Match Inner", outer=True)
|
||||
assemble_color_option(f, "heart_color", get_heart_color_options, "Heart Color", "Red")
|
||||
assemble_color_option(f, "magic_color", get_magic_color_options, "Magic Color", "Green")
|
||||
assemble_color_option(f, "a_button_color", get_a_button_color_options, "A Button Color", "N64 Blue")
|
||||
assemble_color_option(f, "b_button_color", get_b_button_color_options, "B Button Color", "N64 Green")
|
||||
assemble_color_option(f, "c_button_color", get_c_button_color_options, "C Button Color", "Yellow")
|
||||
assemble_color_option(f, "start_button_color", get_start_button_color_options, "Start Button Color", "N64 Red")
|
||||
|
||||
assemble_sfx_option(f, "sfx_navi_overworld", sfx.SoundHooks.NAVI_OVERWORLD, "Navi Overworld")
|
||||
assemble_sfx_option(f, "sfx_navi_enemy", sfx.SoundHooks.NAVI_ENEMY, "Navi Enemy")
|
||||
assemble_sfx_option(f, "sfx_low_hp", sfx.SoundHooks.HP_LOW, "Low HP")
|
||||
assemble_sfx_option(f, "sfx_menu_cursor", sfx.SoundHooks.MENU_CURSOR, "Menu Cursor")
|
||||
assemble_sfx_option(f, "sfx_menu_select", sfx.SoundHooks.MENU_SELECT, "Menu Select")
|
||||
assemble_sfx_option(f, "sfx_nightfall", sfx.SoundHooks.NIGHTFALL, "Nightfall")
|
||||
assemble_sfx_option(f, "sfx_horse_neigh", sfx.SoundHooks.HORSE_NEIGH, "Horse")
|
||||
assemble_sfx_option(f, "sfx_hover_boots", sfx.SoundHooks.BOOTS_HOVER, "Hover Boots")
|
||||
|
||||
print('all done')
|
||||
30
worlds/oot/data/Hints/async.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "async",
|
||||
"gui_name": "Async Multiworld",
|
||||
"description": "Hint distribution intended for large asynchronous multiworlds. WotH disabled, high barren limit, remainder filled with Sometimes and Item hints. No trials hint. Barren hints duplicated.",
|
||||
"add_locations": [],
|
||||
"remove_locations": [],
|
||||
"add_items": [],
|
||||
"remove_items": [],
|
||||
"dungeons_woth_limit": 0,
|
||||
"dungeons_barren_limit": 12,
|
||||
"named_items_required": true,
|
||||
"vague_named_items": false,
|
||||
"distribution": {
|
||||
"trial": {"order": 1, "weight": 0.0, "fixed": 0, "copies": 0},
|
||||
"always": {"order": 2, "weight": 0.0, "fixed": 0, "copies": 1},
|
||||
"woth": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 0},
|
||||
"barren": {"order": 3, "weight": 0.0, "fixed": 5, "copies": 2},
|
||||
"entrance": {"order": 4, "weight": 0.0, "fixed": 4, "copies": 1},
|
||||
"sometimes": {"order": 5, "weight": 6.0, "fixed": 0, "copies": 1},
|
||||
"random": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 1},
|
||||
"item": {"order": 6, "weight": 2.0, "fixed": 0, "copies": 1},
|
||||
"song": {"order": 7, "weight": 2.0, "fixed": 0, "copies": 1},
|
||||
"overworld": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 1},
|
||||
"dungeon": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 1},
|
||||
"junk": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 1},
|
||||
"named-item": {"order": 8, "weight": 0.0, "fixed": 0, "copies": 1}
|
||||
},
|
||||
"groups": [],
|
||||
"disabled": []
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"ADULT_INIT_ITEMS": "03481D2C",
|
||||
"ADULT_VALID_ITEMS": "03481D34",
|
||||
"ADULT_INIT_ITEMS": "03481D40",
|
||||
"ADULT_VALID_ITEMS": "03481D48",
|
||||
"AP_PLAYER_NAME": "03480834",
|
||||
"AUDIO_THREAD_INFO": "03482FAC",
|
||||
"AUDIO_THREAD_INFO_MEM_SIZE": "03482FCC",
|
||||
"AUDIO_THREAD_INFO_MEM_START": "03482FC8",
|
||||
"AUDIO_THREAD_MEM_START": "0348EF10",
|
||||
"AUDIO_THREAD_INFO": "03482FC0",
|
||||
"AUDIO_THREAD_INFO_MEM_SIZE": "03482FDC",
|
||||
"AUDIO_THREAD_INFO_MEM_START": "03482FD8",
|
||||
"AUDIO_THREAD_MEM_START": "0348EF50",
|
||||
"BOMBCHUS_IN_LOGIC": "03480CBC",
|
||||
"CFG_A_BUTTON_COLOR": "03480854",
|
||||
"CFG_A_NOTE_COLOR": "03480872",
|
||||
@@ -16,7 +16,7 @@
|
||||
"CFG_B_BUTTON_COLOR": "0348085A",
|
||||
"CFG_C_BUTTON_COLOR": "03480860",
|
||||
"CFG_C_NOTE_COLOR": "03480878",
|
||||
"CFG_DAMAGE_MULTIPLYER": "03482C9C",
|
||||
"CFG_DAMAGE_MULTIPLYER": "03482CB0",
|
||||
"CFG_DISPLAY_DPAD": "0348088A",
|
||||
"CFG_HEART_COLOR": "0348084E",
|
||||
"CFG_MAGIC_COLOR": "03480848",
|
||||
@@ -36,148 +36,148 @@
|
||||
"CFG_RAINBOW_SWORD_OUTER_ENABLED": "0348088C",
|
||||
"CFG_SHOP_CURSOR_COLOR": "0348086C",
|
||||
"CFG_TEXT_CURSOR_COLOR": "03480866",
|
||||
"CHAIN_HBA_REWARDS": "03483940",
|
||||
"CHEST_SIZE_MATCH_CONTENTS": "034826DC",
|
||||
"COMPLETE_MASK_QUEST": "0348B191",
|
||||
"CHAIN_HBA_REWARDS": "03483950",
|
||||
"CHEST_SIZE_MATCH_CONTENTS": "034826F0",
|
||||
"COMPLETE_MASK_QUEST": "0348B1C9",
|
||||
"COOP_CONTEXT": "03480020",
|
||||
"COOP_VERSION": "03480020",
|
||||
"COSMETIC_CONTEXT": "03480844",
|
||||
"COSMETIC_FORMAT_VERSION": "03480844",
|
||||
"CURRENT_GROTTO_ID": "03482E6E",
|
||||
"DEBUG_OFFSET": "0348288C",
|
||||
"CURRENT_GROTTO_ID": "03482E82",
|
||||
"DEBUG_OFFSET": "034828A0",
|
||||
"DISABLE_TIMERS": "03480CDC",
|
||||
"DPAD_TEXTURE": "0348D710",
|
||||
"DPAD_TEXTURE": "0348D748",
|
||||
"DUNGEONS_SHUFFLED": "03480CDE",
|
||||
"EXTENDED_OBJECT_TABLE": "03480C9C",
|
||||
"EXTERN_DAMAGE_MULTIPLYER": "03482C9D",
|
||||
"EXTERN_DAMAGE_MULTIPLYER": "03482CB1",
|
||||
"FAST_BUNNY_HOOD_ENABLED": "03480CE0",
|
||||
"FAST_CHESTS": "03480CD6",
|
||||
"FONT_TEXTURE": "0348C248",
|
||||
"FONT_TEXTURE": "0348C280",
|
||||
"FREE_SCARECROW_ENABLED": "03480CCC",
|
||||
"GET_CHEST_OVERRIDE_COLOR_WRAPPER": "0348270C",
|
||||
"GET_CHEST_OVERRIDE_SIZE_WRAPPER": "034826E0",
|
||||
"GET_ITEM_TRIGGERED": "034813F8",
|
||||
"GET_CHEST_OVERRIDE_COLOR_WRAPPER": "03482720",
|
||||
"GET_CHEST_OVERRIDE_SIZE_WRAPPER": "034826F4",
|
||||
"GET_ITEM_TRIGGERED": "0348140C",
|
||||
"GOSSIP_HINT_CONDITION": "03480CC8",
|
||||
"GROTTO_EXIT_LIST": "03482E2C",
|
||||
"GROTTO_LOAD_TABLE": "03482DA8",
|
||||
"GROTTO_EXIT_LIST": "03482E40",
|
||||
"GROTTO_LOAD_TABLE": "03482DBC",
|
||||
"INCOMING_ITEM": "03480028",
|
||||
"INCOMING_PLAYER": "03480026",
|
||||
"INITIAL_SAVE_DATA": "0348089C",
|
||||
"JABU_ELEVATOR_ENABLE": "03480CD4",
|
||||
"LACS_CONDITION": "03480CC4",
|
||||
"LACS_CONDITION_COUNT": "03480CD2",
|
||||
"MALON_GAVE_ICETRAP": "0348367C",
|
||||
"MALON_GAVE_ICETRAP": "0348368C",
|
||||
"MALON_TEXT_ID": "03480CDB",
|
||||
"MAX_RUPEES": "0348B193",
|
||||
"MOVED_ADULT_KING_ZORA": "03482FEC",
|
||||
"NO_ESCAPE_SEQUENCE": "0348B15C",
|
||||
"MAX_RUPEES": "0348B1CB",
|
||||
"MOVED_ADULT_KING_ZORA": "03482FFC",
|
||||
"NO_ESCAPE_SEQUENCE": "0348B194",
|
||||
"NO_FOG_STATE": "03480CDD",
|
||||
"OCARINAS_SHUFFLED": "03480CD5",
|
||||
"OPEN_KAKARIKO": "0348B192",
|
||||
"OPEN_KAKARIKO": "0348B1CA",
|
||||
"OUTGOING_ITEM": "03480030",
|
||||
"OUTGOING_KEY": "0348002C",
|
||||
"OUTGOING_PLAYER": "03480032",
|
||||
"OVERWORLD_SHUFFLED": "03480CDF",
|
||||
"PAYLOAD_END": "0348EF10",
|
||||
"PAYLOAD_END": "0348EF50",
|
||||
"PAYLOAD_START": "03480000",
|
||||
"PLAYED_WARP_SONG": "034811FC",
|
||||
"PLAYED_WARP_SONG": "03481210",
|
||||
"PLAYER_ID": "03480024",
|
||||
"PLAYER_NAMES": "03480034",
|
||||
"PLAYER_NAME_ID": "03480025",
|
||||
"RAINBOW_BRIDGE_CONDITION": "03480CC0",
|
||||
"RAINBOW_BRIDGE_COUNT": "03480CD0",
|
||||
"RANDO_CONTEXT": "03480000",
|
||||
"SHUFFLE_BEANS": "03482D04",
|
||||
"SHUFFLE_CARPET_SALESMAN": "034839F8",
|
||||
"SHUFFLE_BEANS": "03482D18",
|
||||
"SHUFFLE_CARPET_SALESMAN": "03483A08",
|
||||
"SHUFFLE_COWS": "03480CD7",
|
||||
"SHUFFLE_MEDIGORON": "03483A54",
|
||||
"SHUFFLE_MEDIGORON": "03483A64",
|
||||
"SONGS_AS_ITEMS": "03480CD8",
|
||||
"SOS_ITEM_GIVEN": "034814C4",
|
||||
"SPEED_MULTIPLIER": "0348274C",
|
||||
"START_TWINROVA_FIGHT": "0348306C",
|
||||
"TIME_TRAVEL_SAVED_EQUIPS": "03481A50",
|
||||
"TRIFORCE_ICON_TEXTURE": "0348DF10",
|
||||
"TWINROVA_ACTION_TIMER": "03483070",
|
||||
"SOS_ITEM_GIVEN": "034814D8",
|
||||
"SPEED_MULTIPLIER": "03482760",
|
||||
"START_TWINROVA_FIGHT": "0348307C",
|
||||
"TIME_TRAVEL_SAVED_EQUIPS": "03481A64",
|
||||
"TRIFORCE_ICON_TEXTURE": "0348DF48",
|
||||
"TWINROVA_ACTION_TIMER": "03483080",
|
||||
"WINDMILL_SONG_ID": "03480CD9",
|
||||
"WINDMILL_TEXT_ID": "03480CDA",
|
||||
"a_button": "0348B120",
|
||||
"a_note_b": "0348B10C",
|
||||
"a_note_font_glow_base": "0348B0F4",
|
||||
"a_note_font_glow_max": "0348B0F0",
|
||||
"a_note_g": "0348B110",
|
||||
"a_note_glow_base": "0348B0FC",
|
||||
"a_note_glow_max": "0348B0F8",
|
||||
"a_note_r": "0348B114",
|
||||
"active_item_action_id": "0348B174",
|
||||
"active_item_fast_chest": "0348B164",
|
||||
"active_item_graphic_id": "0348B168",
|
||||
"active_item_object_id": "0348B16C",
|
||||
"active_item_row": "0348B178",
|
||||
"active_item_text_id": "0348B170",
|
||||
"active_override": "0348B180",
|
||||
"active_override_is_outgoing": "0348B17C",
|
||||
"b_button": "0348B11C",
|
||||
"beating_dd": "0348B128",
|
||||
"beating_no_dd": "0348B130",
|
||||
"c_button": "0348B118",
|
||||
"c_note_b": "0348B100",
|
||||
"c_note_font_glow_base": "0348B0E4",
|
||||
"c_note_font_glow_max": "0348B0E0",
|
||||
"c_note_g": "0348B104",
|
||||
"c_note_glow_base": "0348B0EC",
|
||||
"c_note_glow_max": "0348B0E8",
|
||||
"c_note_r": "0348B108",
|
||||
"cfg_dungeon_info_enable": "0348B0AC",
|
||||
"cfg_dungeon_info_mq_enable": "0348B150",
|
||||
"cfg_dungeon_info_mq_need_map": "0348B14C",
|
||||
"cfg_dungeon_info_reward_enable": "0348B0A8",
|
||||
"cfg_dungeon_info_reward_need_altar": "0348B144",
|
||||
"cfg_dungeon_info_reward_need_compass": "0348B148",
|
||||
"cfg_dungeon_is_mq": "0348B1B0",
|
||||
"cfg_dungeon_rewards": "03489ECC",
|
||||
"cfg_file_select_hash": "0348B158",
|
||||
"cfg_item_overrides": "0348B204",
|
||||
"defaultDDHeart": "0348B134",
|
||||
"defaultHeart": "0348B13C",
|
||||
"dpad_sprite": "0348A040",
|
||||
"dummy_actor": "0348B188",
|
||||
"dungeon_count": "0348B0B0",
|
||||
"dungeons": "03489EF0",
|
||||
"empty_dlist": "0348B0C8",
|
||||
"extern_ctxt": "03489F8C",
|
||||
"font_sprite": "0348A050",
|
||||
"freecam_modes": "03489C4C",
|
||||
"hash_sprites": "0348B0BC",
|
||||
"hash_symbols": "03489FA0",
|
||||
"heap_next": "0348B1AC",
|
||||
"heart_sprite": "03489FE0",
|
||||
"icon_sprites": "03489E10",
|
||||
"item_digit_sprite": "0348A000",
|
||||
"item_overrides_count": "0348B18C",
|
||||
"item_table": "0348A0C8",
|
||||
"items_sprite": "0348A070",
|
||||
"key_rupee_clock_sprite": "0348A010",
|
||||
"last_fog_distance": "0348B0B4",
|
||||
"linkhead_skull_sprite": "03489FF0",
|
||||
"medal_colors": "03489EDC",
|
||||
"medals_sprite": "0348A080",
|
||||
"normal_dd": "0348B124",
|
||||
"normal_no_dd": "0348B12C",
|
||||
"object_slots": "0348C204",
|
||||
"pending_freezes": "0348B190",
|
||||
"pending_item_queue": "0348B1EC",
|
||||
"quest_items_sprite": "0348A060",
|
||||
"rupee_colors": "03489E1C",
|
||||
"satisified_pending_frames": "0348B160",
|
||||
"scene_fog_distance": "0348B0B8",
|
||||
"setup_db": "0348A0A0",
|
||||
"song_note_sprite": "0348A020",
|
||||
"stones_sprite": "0348A090",
|
||||
"text_cursor_border_base": "0348B0D4",
|
||||
"text_cursor_border_max": "0348B0D0",
|
||||
"text_cursor_inner_base": "0348B0DC",
|
||||
"text_cursor_inner_max": "0348B0D8",
|
||||
"triforce_hunt_enabled": "0348B1A0",
|
||||
"triforce_pieces_requied": "0348B142",
|
||||
"triforce_sprite": "0348A030"
|
||||
"a_button": "0348B158",
|
||||
"a_note_b": "0348B144",
|
||||
"a_note_font_glow_base": "0348B12C",
|
||||
"a_note_font_glow_max": "0348B128",
|
||||
"a_note_g": "0348B148",
|
||||
"a_note_glow_base": "0348B134",
|
||||
"a_note_glow_max": "0348B130",
|
||||
"a_note_r": "0348B14C",
|
||||
"active_item_action_id": "0348B1AC",
|
||||
"active_item_fast_chest": "0348B19C",
|
||||
"active_item_graphic_id": "0348B1A0",
|
||||
"active_item_object_id": "0348B1A4",
|
||||
"active_item_row": "0348B1B0",
|
||||
"active_item_text_id": "0348B1A8",
|
||||
"active_override": "0348B1B8",
|
||||
"active_override_is_outgoing": "0348B1B4",
|
||||
"b_button": "0348B154",
|
||||
"beating_dd": "0348B160",
|
||||
"beating_no_dd": "0348B168",
|
||||
"c_button": "0348B150",
|
||||
"c_note_b": "0348B138",
|
||||
"c_note_font_glow_base": "0348B11C",
|
||||
"c_note_font_glow_max": "0348B118",
|
||||
"c_note_g": "0348B13C",
|
||||
"c_note_glow_base": "0348B124",
|
||||
"c_note_glow_max": "0348B120",
|
||||
"c_note_r": "0348B140",
|
||||
"cfg_dungeon_info_enable": "0348B0E4",
|
||||
"cfg_dungeon_info_mq_enable": "0348B188",
|
||||
"cfg_dungeon_info_mq_need_map": "0348B184",
|
||||
"cfg_dungeon_info_reward_enable": "0348B0E0",
|
||||
"cfg_dungeon_info_reward_need_altar": "0348B17C",
|
||||
"cfg_dungeon_info_reward_need_compass": "0348B180",
|
||||
"cfg_dungeon_is_mq": "0348B1E8",
|
||||
"cfg_dungeon_rewards": "03489EDC",
|
||||
"cfg_file_select_hash": "0348B190",
|
||||
"cfg_item_overrides": "0348B23C",
|
||||
"defaultDDHeart": "0348B16C",
|
||||
"defaultHeart": "0348B174",
|
||||
"dpad_sprite": "0348A050",
|
||||
"dummy_actor": "0348B1C0",
|
||||
"dungeon_count": "0348B0E8",
|
||||
"dungeons": "03489F00",
|
||||
"empty_dlist": "0348B100",
|
||||
"extern_ctxt": "03489F9C",
|
||||
"font_sprite": "0348A060",
|
||||
"freecam_modes": "03489C5C",
|
||||
"hash_sprites": "0348B0F4",
|
||||
"hash_symbols": "03489FB0",
|
||||
"heap_next": "0348B1E4",
|
||||
"heart_sprite": "03489FF0",
|
||||
"icon_sprites": "03489E20",
|
||||
"item_digit_sprite": "0348A010",
|
||||
"item_overrides_count": "0348B1C4",
|
||||
"item_table": "0348A0D8",
|
||||
"items_sprite": "0348A080",
|
||||
"key_rupee_clock_sprite": "0348A020",
|
||||
"last_fog_distance": "0348B0EC",
|
||||
"linkhead_skull_sprite": "0348A000",
|
||||
"medal_colors": "03489EEC",
|
||||
"medals_sprite": "0348A090",
|
||||
"normal_dd": "0348B15C",
|
||||
"normal_no_dd": "0348B164",
|
||||
"object_slots": "0348C23C",
|
||||
"pending_freezes": "0348B1C8",
|
||||
"pending_item_queue": "0348B224",
|
||||
"quest_items_sprite": "0348A070",
|
||||
"rupee_colors": "03489E2C",
|
||||
"satisified_pending_frames": "0348B198",
|
||||
"scene_fog_distance": "0348B0F0",
|
||||
"setup_db": "0348A0B0",
|
||||
"song_note_sprite": "0348A030",
|
||||
"stones_sprite": "0348A0A0",
|
||||
"text_cursor_border_base": "0348B10C",
|
||||
"text_cursor_border_max": "0348B108",
|
||||
"text_cursor_inner_base": "0348B114",
|
||||
"text_cursor_inner_max": "0348B110",
|
||||
"triforce_hunt_enabled": "0348B1D8",
|
||||
"triforce_pieces_requied": "0348B17A",
|
||||
"triforce_sprite": "0348A040"
|
||||
}
|
||||