Compare commits

..

115 Commits

Author SHA1 Message Date
Fabian Dill
1cef10b309 Timespinner: hide it for now 2021-09-25 01:13:50 +02:00
espeon65536
c3070be14a Update small and boss key counters during the normal update cycle 2021-09-24 23:10:26 +00:00
espeon65536
5570440eb1 Ocarina of Time webtracker 2021-09-24 18:44:25 +00:00
espeon65536
ec0a5df5a1 give Song from Impa and ZL as starting items if skip_child_zelda is on 2021-09-24 18:44:25 +00:00
Edos512
8411b76ee5 Update minecraft_es.md (#80)
* Update minecraft_es.md

Updated spanish minecraft tutorial
2021-09-24 20:42:35 +02:00
Jarno Westhof
822e8941ed Added Timespinner support (#77)
AP side for 0.1.8 inclusion, Client and Documentation outstanding.
2021-09-24 04:07:32 +02:00
Fabian Dill
7ac9bd8591 tracker.py: run Reformat Code 2021-09-23 13:52:32 +02:00
Fluffyhairedguy
68a5784650 New column for generic tracker (#78)
* Adding order received column to generic tracker. Progressive items will have the most recent number only.
2021-09-23 13:48:25 +02:00
Fabian Dill
67f324b939 Spoiler: remove duplicate start inventory entries 2021-09-23 04:08:36 +02:00
Fabian Dill
8db8c60e75 Core: fix start_inventory ignoring count 2021-09-23 03:53:16 +02:00
Fabian Dill
8e569a1d1f AutoWorld: split remote_start_inventory out from remote_items 2021-09-23 03:48:37 +02:00
Fabian Dill
3caf8bc82b WebHost: Allow plando
Maybe move to a different webpage?
2021-09-23 02:29:24 +02:00
Fabian Dill
3da028415f Factorio: fix random rocket recipe 2021-09-22 08:08:57 +02:00
Fabian Dill
104df1915d UI: no longer close Clients on escape key press 2021-09-22 08:08:38 +02:00
CaitSith2
bfb6d44195 Fix failure to roll seeds with silo: randomize_recipe 2021-09-21 23:05:14 -07:00
Chris Wilson
df0e8bc027 Remove aliased options from player-settings pages 2021-09-22 00:21:57 -04:00
Fabian Dill
442b6ced35 Docs: Update network graph 2021-09-20 12:15:31 +02:00
Fabian Dill
111e11924f LttP: fix multithreading racing condition resulting in Ganon giving the wrong prog bow hint, also have one less world.find_items() which is quite cpu expensive 2021-09-20 01:00:09 +02:00
espeon65536
061cc69a6a Convert color and sfx options into top-level definitions for pickling 2021-09-19 05:23:10 +00:00
espeon65536
f9950e1f01 add comment for suns song 2021-09-19 05:23:10 +00:00
espeon65536
895d259589 correctly write memory address for Song from Composers Grave so it's always recognized by client 2021-09-19 05:23:10 +00:00
Chris Wilson
4ea80f34fa Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into main 2021-09-18 16:15:50 -04:00
Chris Wilson
77878bf714 Fill out game info pages for LttP, OoT, Factorio, and Subnautica. Revert MD pages to stop using simple line breaks. 2021-09-18 16:15:40 -04:00
Fabian Dill
f85dde6323 LttP: remove rom handling from Main.py 2021-09-18 22:13:19 +02:00
Fabian Dill
6441f92c9f LttP: remove no longer used argument 2021-09-18 06:56:19 +02:00
Chris Wilson
25b9fc8b6a Better wording for player-settings reset banner 2021-09-17 21:24:52 -04:00
Chris Wilson
090678776e Add version hashing to player-settings pages 2021-09-17 21:23:31 -04:00
Chris Wilson
9be6d443d7 Fix /gameInfo pages not loading markdown correctly 2021-09-17 20:39:53 -04:00
Chris Wilson
678253d037 Fix /games page not working 2021-09-17 20:35:31 -04:00
Fabian Dill
bd561fd191 WebHost: fix Py39(only?) jinja weirdness with undefined attribute checking 2021-09-18 01:32:34 +02:00
Fabian Dill
38b5ee7314 WebHost: working web-gen 2021-09-18 01:02:26 +02:00
Chris Wilson
11245462f0 Added gameInfo page using markdown, removed old game sub-pages and directories 2021-09-17 18:41:26 -04:00
Fabian Dill
351a5b87bf Setup: Make OoT and LttP Rom optional components to the Generator 2021-09-17 10:09:03 +02:00
Fabian Dill
b780257098 MultiServer: fix IgnoreGame missing 'not' 2021-09-17 04:35:38 +02:00
Fabian Dill
4e1f1551ea Subnautica: add 'valuable' item_pool 2021-09-17 04:32:36 +02:00
Fabian Dill
b82e3f2a8a MultiServer: Split InvalidSlot out into InvalidGame and document all error codes. 2021-09-17 04:32:09 +02:00
Fabian Dill
a82bf1bb32 Options: raise Exception if per-game options are in root
Options: implement progression balancing and accessibility on new system
Options: implement the notion of "common" and "per_game_common" options in various systems
Options: centralize item and location name checking
Spoiler: prettier print some lists, sets and dicts
WebHost: add common options into /templates
2021-09-17 00:17:54 +02:00
Chris Wilson
abc0220cfa Include the game name in the generated JSON files used to populate player-settings pages 2021-09-16 17:15:25 -04:00
espeon65536
f17e6f9afd Ensure removed items and events do not appear in the starting inventory multidata and web tracker 2021-09-15 10:40:36 +00:00
espeon65536
16e6b9eed7 Ensure that Sheik in Ice Cavern doesn't get a dungeon item 2021-09-15 10:40:36 +00:00
espeon65536
323415ba9c allow gossip hints for light arrows with either vanilla bridge or nonzero trials required 2021-09-15 10:40:36 +00:00
espeon65536
ae97b5e704 Fix drawing AP items in shops 2021-09-15 10:40:36 +00:00
espeon65536
6b8b30c3c7 fix skull token ranges 2021-09-15 10:40:36 +00:00
espeon65536
0df2b2221d Separate triforce pieces in pool from the item pool setting 2021-09-15 10:40:36 +00:00
espeon65536
e2b36dfa7d remove debug print 2021-09-15 10:40:36 +00:00
espeon65536
4e18f24f3b Add glitchless condition to ganon's castle junk fill 2021-09-15 10:40:36 +00:00
espeon65536
b0d5a51768 Add proportional junk fill to Ganon's Castle 2021-09-15 10:40:36 +00:00
espeon65536
b3d2c22373 accidentally optimized a little too much 2021-09-15 10:40:36 +00:00
espeon65536
cace88e8fa Reenable Chest Size Matches Contents 2021-09-15 10:40:36 +00:00
espeon65536
9c09d84c71 Make AP items into Zelda's Letter, with custom text and proper sfx for advancement 2021-09-15 10:40:36 +00:00
espeon65536
2d27665369 Fix shop items having inconsistent save context information, causing shops to not be sent correctly if fewer than 4 items in any shop 2021-09-15 10:40:36 +00:00
espeon65536
45266caa8d make logic_tricks section in playerSettings clearer 2021-09-15 10:40:36 +00:00
espeon65536
feb1a59902 remove unreachable code in _oot_can_live_dmg 2021-09-15 10:40:36 +00:00
espeon65536
fdec4157da Skip looping over every location in set_rules and set_entrances_based_rules, use filter instead 2021-09-15 10:40:36 +00:00
espeon65536
4e84b20925 optimize set_shop_rules 2021-09-15 10:40:36 +00:00
espeon65536
f952ad5913 turn on guarantee_hint rule 2021-09-15 10:40:36 +00:00
espeon65536
be27586203 make stage_generate_output a class method 2021-09-15 10:40:36 +00:00
espeon65536
9dc3f3f38b Hint generation improvements
Only generate the required hint data for a world based on its hint distribution
Set various major items as nonprogression never_exclude based on settings
2021-09-15 10:40:36 +00:00
espeon65536
f39defbe06 Add "async" hint distribution 2021-09-15 10:40:36 +00:00
espeon65536
890f71a477 fix bug causing songs to never be hinted 2021-09-15 10:40:36 +00:00
espeon65536
bc8e8c5daf add oot ROM selection to inno_setup 2021-09-15 10:40:36 +00:00
espeon65536
37f12809a1 commented out some junk hints unsuitable for AP 2021-09-15 10:40:36 +00:00
espeon65536
f5c0b847a9 make defaults for LacsTokens and BridgeTokens not insane 2021-09-15 10:40:36 +00:00
espeon65536
44d6c3c07e oot updates to playerSettings 2021-09-15 10:40:36 +00:00
espeon65536
da1a2b2957 split shopsanity into two options: "shopsanity" and "shop_slots" 2021-09-15 10:40:36 +00:00
espeon65536
9f6fa2bd05 Rework __init__ to use create_items and pre_fill properly
Puts keys into the itempool along with all other items
Fixes a bug where dungeon smallkeys + nondungeon big keys fails generation
Also includes some minor optimizations mostly relating to iterables
2021-09-15 10:40:36 +00:00
Fabian Dill
5d68dc568f Fill: fix non_local_items breaking in single player 2021-09-15 01:02:06 +02:00
Fabian Dill
ee1ea881e8 LttP: fix Enemizer option handover 2021-09-15 00:24:52 +02:00
Fabian Dill
87add88436 Factorio: add stone as red science option 2021-09-13 23:50:43 +02:00
Fabian Dill
7643609e09 Factorio: add iron ore, copper ore and coal to red science pool 2021-09-13 23:26:45 +02:00
Fabian Dill
007a393ab5 Generate: don't count the 0th output file. 2021-09-13 03:38:18 +02:00
Fabian Dill
80c90c0a00 LttP: why is item pool called difficulty again? 2021-09-13 02:03:59 +02:00
Fabian Dill
c1c92647ca LttP: move some simple Toggle options over to new system part 2 2021-09-13 02:01:15 +02:00
Fabian Dill
033adceb6f LttP: move some simple Toggle options over to new system 2021-09-13 01:32:32 +02:00
Fabian Dill
e57e92bfee CommonClient: reduce blind sleep time of keep_alive 2021-09-12 21:15:37 +02:00
Fabian Dill
a1a7729c3b Docs: point to existing further documentation. 2021-09-11 22:44:48 +02:00
Fabian Dill
071b0eeb77 MultiServer: add datapackage legacy warning 2021-09-11 22:37:24 +02:00
Fabian Dill
fafc17c7d3 Risk of Rain 2: fix missing ItemPickup location (off by one itempool) 2021-09-11 22:14:39 +02:00
Fabian Dill
7599302920 CommonClient: remove leftover debug print 2021-09-11 22:07:54 +02:00
Hussein Farran
7f8d7231a4 Merge pull request #71 from SolventMercury/main
Add documentation for adding games to Archipelago
2021-09-11 15:55:35 -04:00
Fabian Dill
b1196885d7 CommonClient: implement active keep-alive 2021-09-11 03:59:12 +02:00
Fabian Dill
494cfb3c04 Setup Guides: update LttP en and de guides with SNI 2021-09-10 15:20:45 +02:00
Fabian Dill
6a65981103 Plando: support Item plando on any game (up from only LttP) 2021-09-10 04:28:06 +02:00
Fabian Dill
f508f93d69 Risk of Rain 2: fix lunar item removal affects all following worlds' presets 2021-09-10 04:11:01 +02:00
Fabian Dill
411d4434a3 MultiServer: update to websockets 10 and implement new websockets.broadcast 2021-09-09 18:56:52 +02:00
CaitSith2
d41fce6f91 Check if starting item actually exists before trying to give it to player. 2021-09-09 07:44:45 -07:00
Fabian Dill
282e7b4006 FactorioClient: End the log on "No Archipelago mod was loaded. Aborting." if no bridge mod was found.
CommonClient: give separate error for invalid URI
2021-09-09 16:02:45 +02:00
Hussein Farran
b4c3c5deea Merge pull request #70 from alwaysintreble/main
Risk of Rain 2 dynamic item pool
2021-09-08 15:44:47 -04:00
Hussein Farran
683514d891 Merge branch 'main' into main 2021-09-08 15:03:19 -04:00
alwaysintreble
e9beb21a98 Adjusted chaos preset weights to be a bit more chaotic and optimized item pool generation a bit. 2021-09-08 13:53:06 -05:00
Hussein Farran
bc47f78264 Remove colons in headings. 2021-09-08 13:46:31 -04:00
Hussein Farran
b002f7f862 Update document with relative images and links, as well as updated language and formatting. 2021-09-08 13:43:39 -04:00
alwaysintreble
05dac999a8 Fixed chaos item weight preset so it gets generated per world. 2021-09-08 12:29:29 -05:00
SolventMercury
242595b725 Added guide for adding new games to AP 2021-09-08 07:05:19 -07:00
SolventMercury
48dd1a1aa6 Revert "Add Terraria Support"
This reverts commit 3aacaffe6b.
2021-09-08 07:02:12 -07:00
SolventMercury
3aacaffe6b Add Terraria Support
But it works this time.
Hopefully.
Client still needs to be caught up.
2021-09-08 05:58:34 -07:00
Chris Wilson
61b875256f Add table styling to markdown css 2021-09-07 19:41:04 -04:00
Chris Wilson
14dc450631 Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into main 2021-09-07 19:22:18 -04:00
Chris Wilson
6352056528 Enable QOL features in showdown extension 2021-09-07 19:22:04 -04:00
alwaysintreble
bd4f24844b Update documentation for new options. 2021-09-07 17:50:43 -05:00
alwaysintreble
062615b6b1 Revert "Added descriptions to all currently existing options. Please end me"
This reverts commit 29ed40051d.
2021-09-07 17:24:25 -05:00
alwaysintreble
6c9293e4f6 Added a dynamicallly loaded item weight pool with presets. 2021-09-07 17:14:20 -05:00
alwaysintreble
24802d64c7 Reverted some changes 2021-09-07 09:22:12 -05:00
alwaysintreble
5e8a686bb6 Merge remote-tracking branch 'AP_upstream/main' into dev 2021-09-07 08:29:36 -05:00
Jarno Westhof
279ab89a61 Fixed Typo 2021-09-07 08:27:44 +00:00
alwaysintreble
29ed40051d Added descriptions to all currently existing options. Please end me 2021-09-06 23:58:59 -05:00
alwaysintreble
8d05aa6262 Added a custom dynamic item weights pool option. 2021-09-06 23:40:39 -05:00
alwaysintreble
694f942c06 Renamed RiskOfRainItem to RiskOfRain2Item to prevent any potential problems if someone adds Risk of Rain 1 2021-09-06 23:39:27 -05:00
Fabian Dill
105a2d4e13 WebHost: make LttP sprites optional 2021-09-07 00:42:02 +02:00
Hussein Farran
1ee62912fd Merge branch 'main' into main 2021-09-06 13:19:33 -04:00
Hussein Farran
abacca34ee Add startWithDio to slot_data. 2021-09-06 13:15:07 -04:00
Fainspirit
3e4e69735e Fixed awkward phrasing in FAQ 2021-09-05 12:38:07 +00:00
Chris Wilson
4afc351933 Fill out FAQ on the website 2021-09-04 19:23:09 -04:00
Fabian Dill
23b8070b9d Options: allow comparing Choices with other Choices 2021-09-04 17:53:09 +02:00
Hussein Farran
df435eb693 Remove total_items option. 2021-09-01 17:35:16 -04:00
114 changed files with 17201 additions and 13170 deletions

View File

@@ -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()]))

View File

@@ -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:

View File

@@ -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()

View File

@@ -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)

View File

@@ -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
View File

@@ -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:

View File

@@ -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:

View File

@@ -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):

View File

@@ -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

View File

@@ -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" %

View File

@@ -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"]:

View File

@@ -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>')

View File

@@ -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:

View File

@@ -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.")

View File

@@ -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,

View File

@@ -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();

View File

@@ -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.

View 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>`;
});
});

View 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!

View 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.

View 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!

View 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
View 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);

View 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;
});
}
});

View File

@@ -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 = () => {

View File

@@ -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();

View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;
}

View 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%);
}

View File

@@ -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;
}

View 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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 -%}

View File

@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ player_name }}&apos;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>

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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)

View File

@@ -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
View 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. Its 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
![Creepy Castle Root Directory in Window's Explorer](./img/creepy-castle-directory.png)
This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. Its also your worst-case
scenario as a modder. All thats 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. Lets look at some other examples
of game releases.
### Heavy Bullets
![Heavy Bullets Root Directory in Window's Explorer](./img/heavy-bullets-directory.png)
Heres 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 wont 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, its just some code used to interface with Steam.
The directory “HEAVY_BULLETS_Data”, however, has some good news.
![Heavy Bullets Data Directory in Window's Explorer](./img/heavy-bullets-data-directory.png)
Jackpot! It might not be obvious what youre looking at here, but I can instantly tell from this folders contents that
what we have is a game made in the Unity Engine. If you look in the sub-folders, youll 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. Well 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,
thats another dead giveaway.
### Stardew Valley
![Stardew Valley Root Directory in Window's Explorer](./img/stardew-valley-directory.png)
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
![Gato Roboto Root Directory in Window's Explorer](./img/gato-roboto-directory.png)
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 whats 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 games 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 youll want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as pictured below:
![Heavy Bullets Managed Directory in Window's Explorer](./img/heavy-bullets-managed-directory.png)
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 games C# code, but the tool isnt 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 wont 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 isnt that old, its 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
Thats 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 consoles secrets.
Look for debugging tools, but be ready to learn assembly.
Old consoles usually have their own unique dialects of ASM youll need to get used to.
Also make sure theres a good way to interface with a running emulator, since thats 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. Its 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 youd 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 cant 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 elses original work.
Users who have a copy of the game just need to apply the patch, and those who dont 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 mods 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 players yaml file, and the code to initialize all this data.
Heres an example of what your world module can look like:
![Example world module directory open in Window's Explorer](./img/archipelago-world-directory-example.png)
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.
![Example Items.py file open in Notepad++](./img/example-items-py-file.png)
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.
![Example Locations.py file open in Notepad++](./img/example-locations-py-file.png)
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.
![Example Options.py file open in Notepad++](./img/example-options-py-file.png)
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.
![Example Regions.py file open in Notepad++](./img/example-regions-py-file.png)
`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.
![Example Rules.py file open in Notepad++](./img/example-rules-py-file.png)
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.
![Example \_\_init\_\_.py file open in Notepad++](./img/example-init-py-file.png)
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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -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;

View File

@@ -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"))

View File

@@ -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

View File

@@ -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

View File

@@ -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']

View File

@@ -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)

View File

@@ -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)

View File

@@ -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']

View File

@@ -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']

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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})

View File

@@ -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":

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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 = [

View File

@@ -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'])

View File

@@ -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)

View File

@@ -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.

View File

@@ -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 = {

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View 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')

View 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": []
}

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}

Some files were not shown because too many files have changed in this diff Show More