Compare commits

..

35 Commits

Author SHA1 Message Date
CaitSith2
63fb888191 Now possible to run generated modded seeds even without the json files. 2022-09-25 07:38:05 -07:00
CaitSith2
38eef5ac00 Remaining changes
* Process burnt results.
* Add fluidbox info to machine definition
* Recipe base cost now uses cheapest product source.
* free sample exclusions no longer hard-coded
* science pack pool exclusions no longer hard-coded
* burnt result is forced to have cost of machine used to obtain the results factored into the cost of crafting.
* name of machine is no longer assumed to be name of item.  machine definition now list item sources specifically.
* science pack difficulty is now the minimum of average estimated difficulty of unlocked recipes and 8.
* Actually exclude archipelago-extractor from mod list.
2022-09-25 04:09:14 -07:00
CaitSith2
3e627f80fd Items and Fluids now have their own list of product_sources 2022-09-25 03:35:07 -07:00
CaitSith2
0d6aeea9fd Cut back on the recursion loop false positives. 2022-09-25 03:31:35 -07:00
CaitSith2
6cd1e8a295 Resolve mining energy TODO 2022-09-25 03:28:41 -07:00
CaitSith2
1cbe5ae669 Fluids now a FactorioElement. 2022-09-25 03:27:55 -07:00
CaitSith2
c5b5ad495c Merge branch 'main' into factorio_data_extraction_refactor 2022-09-24 02:50:16 -07:00
CaitSith2
813ee5ee3b Factorio: Add explicit support for factory-levels mod. (#1050)
* Factorio: Add explicit support for factory-levels mod.

* Fix inconsistent space/tabs
2022-09-24 02:43:00 -07:00
Fabian Dill
be1158ad78 Windows: update VC Redistributable to 14.32.31332 from 14.29.30037 2022-09-22 08:46:48 +02:00
CaitSith2
5b3f4460b8 remove debug print statements. 2022-09-21 03:07:19 -07:00
CaitSith2
de8eff39b3 Refactoring the data extraction for factorio
Extracted more information with a modified version of Archipelago extractor.   Matching PR for the extractor on its respective github.
2022-09-21 02:42:59 -07:00
black-sliver
6d5ddf3cad MultiServer: allow using IDs for hints 2022-09-20 18:38:31 +02:00
black-sliver
809bda02d1 Test: item/location name must not be numeric 2022-09-20 18:38:16 +02:00
black-sliver
2d5ec6ce22 Doc: item/location name must not be numeric 2022-09-20 18:38:16 +02:00
black-sliver
a95d0ce9ef Doc: clarify requirements.txt in world api.md 2022-09-20 09:48:30 +02:00
alwaysintreble
267d9234e5 core: fix options with "random" as default value not generating (#1033)
* core: fix options with "random" as default value not generating

when option is missing from the player yaml,

Using this in #893 and tested there.

* remove if

* OptionSets default to frozenset so handle that

* range had some specific instances of assuming default as a valid value so change this here to call the from_any

* isinstance instead of type

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-19 22:40:15 +02:00
black-sliver
4686881566 WebHost: CustomServer: use defaultdicts
also change non_hintable to defaultdict in MultiServer and add some typing
2022-09-19 01:20:36 +02:00
SoldierofOrder
101dab0ea4 SC2: Add helpful feedback when failing to locate SC2 (#1032)
* SC2: The client now throws a descriptive error when ExecuteInfo.txt exists but is empty, and offers more helpful suggestions when the file doesn't exist.

* SC2: Replaced the new RuntimeError with a warning in the logger to keep things consistent.

* Removed communism

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-18 16:48:36 +02:00
Fabian Dill
c2d69cb05e Core: add generic interface to add ER data to hints (#1014) 2022-09-18 14:30:43 +02:00
black-sliver
58f66e0f42 autoworld: don't load files/folders starting with '.' (#1030)
* autoworld: don't load files/folders starting with '.'

The imports fail if the folder has a '.' in the name, with a somewhat obscure error, and adding a '.' in front of it is what a linux user might expect to use when disabling a world temporarily.

* autoworld: use tuple to filter .* and _*
2022-09-18 13:02:05 +02:00
Fabian Dill
0215e1fa28 SC2: always show uncollected locations (#1007) 2022-09-18 12:40:35 +02:00
black-sliver
1c0a93acad doc: update use of relative/absolute imports
it matters for apworlds to function
2022-09-18 10:22:17 +02:00
NewSoupVi
4fcde135e5 The Witness: Renaming, Options, Logic Fixes (#1000)
Fixes to postgame detection for "shuffle_postgame"
Renamed many locations to be symbol-independent ("Outside Tutorial Dots Introduction" becomes "Outside Tutorial Shed Row"). This is to set up future alternate modes, like Sigma Expert, which use completely different symbols.
Renamed most door items to be shorter, more consistent, and less... stupid. ("Bunker Bunker Entry Door" -> "Bunker Entry")
Removed "shuffle_uncommon"
Many logic fixes
2022-09-18 04:20:59 +02:00
alwaysintreble
332dde154f core: new freetext and textchoice options (#728)
* add freetext and freetextchoice options

* fix textchoice. create plando_bosses bool so worlds can check if boss plando is enabled

* remove strange unneccessary \ escapes

* lttp: rip boss plando out of core

* fix broken text methods so they read the data correctly

* revert `None` key in boss_shuffle_options. fix failing tests

* lttp: rewrite boss plando

* lttp: rewrite boss shuffle

* add generic verification step and allow options to set a plando module

* add default typing to plando_options set

* use PlandoSettings intflag for lttp boss plando

* fix plandosettings boss flag check

* minor lttp init cleanup

* make suggested changes. account for "random" existing within plando boss options

* override eq operator

* Please document me!

* Forgot to mention it supports plando

* remove auto_display_name

* Throw warning alerting user to which shuffle is being used if plando is off. Set the remaining boss shuffle in init and boss placement cleanup

* move the convoluted string matching to `from_text`

* remove unneccessary text lowering and actually turn off plando option when it's disabled

* typing

* strong typing for verify method and reorder

* typing is your friend

* log warning correctly

* 3.8 support :(

* also list apparently

* rip out old boss shuffle spoiler code

* verification step for plando bosses and locations

* update plando guide to reference new supported behavior

* empty string is not `None`. remove unneccessary error throw

* Fix bad ordering

* validate boss_shuffle only contains a normal boss option at the end

* get random choice from a list dummy

* >:(

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* minor textchoice cleanup

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-17 02:55:33 +02:00
black-sliver
8d51205e8f Alttp: only check item.type for own items with retro_cave 2022-09-17 02:25:09 +02:00
black-sliver
ff05e9d7d5 MultiServer: produce nicer output ...
... for headless and when cancelling the file open dialog
2022-09-17 02:24:51 +02:00
N00byKing
516a52c041 sm64ex: Fix WDW 1Up Block Logic 2022-09-17 02:13:32 +02:00
Alchav
9daa64741b New, smarter fast_fill function (#646)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2022-09-17 02:06:25 +02:00
Fabian Dill
af11fa5150 Core: auto alias (#1022)
* Test: check that default templates can be parsed into Option objects
2022-09-16 00:32:30 +02:00
TheBigSalarius
156e9e0e43 FF1: Throw exception for Noverworld 2022-09-12 03:48:07 +02:00
Fabian Dill
ef46979bd8 SC2: fix client freezing on exit while SC2 is running (#1011) 2022-09-12 02:08:33 +02:00
Fabian Dill
b2aa251c47 SC2: fix bitflag overflow when multiple instances of an Item are acquired (#1008) 2022-09-12 01:51:25 +02:00
Fabian Dill
e204a0fce6 Subnautica: fix missed item and correct other item pool counts to fit it 2022-09-12 01:44:10 +02:00
TheBigSalarius
bb386d3bd7 FF1: fix FF1Client messaging and scoped lua messaging with printjson
Corrects the issue causing the client and lua messaging not displaying properly after the printjson changes
2022-09-12 01:19:51 +02:00
Fabian Dill
88a225764a FF1: fix printjson 2022-09-12 01:19:51 +02:00
100 changed files with 3078 additions and 2738 deletions

1
.gitignore vendored
View File

@@ -21,7 +21,6 @@
*.archipelago
*.apsave
docs/sphinx/_build/
build
bundle/components.wxs
dist

View File

@@ -955,6 +955,13 @@ class Region:
return True
return False
def get_connecting_entrance(self, is_main_entrance: typing.Callable[[Entrance], bool]) -> Entrance:
for entrance in self.entrances:
if is_main_entrance(entrance):
return entrance
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
def __repr__(self):
return self.__str__()
@@ -1422,7 +1429,6 @@ class Spoiler():
"f" in self.world.shop_shuffle[player]))
outfile.write('Custom Potion Shop: %s\n' %
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('Prize shuffle %s\n' %

View File

@@ -5,6 +5,7 @@ import urllib.parse
import sys
import typing
import time
import functools
import ModuleUpdate
ModuleUpdate.update()
@@ -17,7 +18,8 @@ if __name__ == "__main__":
Utils.init_logging("TextClient", exception_logger="Client")
from MultiServer import CommandProcessor
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
from Utils import Version, stream_input
from worlds import network_data_package, AutoWorldRegister
import os
@@ -204,6 +206,10 @@ class CommonContext:
# execution
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
@functools.cached_property
def raw_text_parser(self) -> RawJSONtoTextParser:
return RawJSONtoTextParser(self)
@property
def total_locations(self) -> typing.Optional[int]:
"""Will return None until connected."""

View File

@@ -1,4 +1,5 @@
import asyncio
import copy
import json
import time
from asyncio import StreamReader, StreamWriter
@@ -6,7 +7,7 @@ from typing import List
import Utils
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
SYSTEM_MESSAGE_ID = 0
@@ -64,7 +65,7 @@ class FF1Context(CommonContext):
def _set_message(self, msg: str, msg_id: int):
if DISPLAY_MSGS:
self.messages[(time.time(), msg_id)] = msg
self.messages[time.time(), msg_id] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
@@ -73,32 +74,28 @@ class FF1Context(CommonContext):
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == 'PrintJSON':
print_type = args['type']
item = args['item']
receiving_player_id = args['receiving']
receiving_player_name = self.player_names[receiving_player_id]
sending_player_id = item.player
sending_player_name = self.player_names[item.player]
if print_type == 'Hint':
msg = f"Hint: Your {self.item_names[item.item]} is at" \
f" {self.player_names[item.player]}'s {self.location_names[item.location]}"
self._set_message(msg, item.item)
elif print_type == 'ItemSend' and receiving_player_id != self.slot:
if sending_player_id == self.slot:
if receiving_player_id == self.slot:
msg = f"You found your own {self.item_names[item.item]}"
else:
msg = f"You sent {self.item_names[item.item]} to {receiving_player_name}"
else:
if receiving_player_id == sending_player_id:
msg = f"{sending_player_name} found their {self.item_names[item.item]}"
else:
msg = f"{sending_player_name} sent {self.item_names[item.item]} to " \
f"{receiving_player_name}"
def on_print_json(self, args: dict):
if self.ui:
self.ui.print_json(copy.deepcopy(args["data"]))
else:
text = self.jsontotextparser(copy.deepcopy(args["data"]))
logger.info(text)
relevant = args.get("type", None) in {"Hint", "ItemSend"}
if relevant:
item = args["item"]
# goes to this world
if self.slot_concerns_self(args["receiving"]):
relevant = True
# found in this world
elif self.slot_concerns_self(item.player):
relevant = True
# not related
else:
relevant = False
if relevant:
item = args["item"]
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
self._set_message(msg, item.item)
def run_gui(self):

View File

@@ -65,6 +65,7 @@ class FactorioContext(CommonContext):
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
self.energy_link_increment = 0
self.last_deplete = 0
self.custom_data_package = 0
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -170,7 +171,7 @@ async def game_watcher(ctx: FactorioContext):
if ctx.locations_checked != research_data:
bridge_logger.debug(
f"New researches done: "
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
f"{[lookup_id_to_name.get(rid, f'Unknown Research (ID: {rid})') for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
death_link_tick = data.get("death_link_tick", 0)
@@ -268,7 +269,11 @@ async def factorio_server_watcher(ctx: FactorioContext):
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
item_id = transfer_item.item
player_name = ctx.player_names[transfer_item.player]
if item_id not in Factorio.item_id_to_name:
if ctx.custom_data_package:
item_name = Factorio.item_id_to_name.get(item_id, f"Unknown Item (ID: {item_id})")
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.{(' (Item name might not match the seed.)' if Factorio.data_version else '')}")
commands[ctx.send_index] = f'/ap-get-technology {item_id}\t{ctx.send_index}\t{player_name}'
elif item_id not in Factorio.item_id_to_name:
factorio_server_logger.error(f"Cannot send unknown item ID: {item_id}")
else:
item_name = Factorio.item_id_to_name[item_id]
@@ -297,6 +302,7 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
# 0.2.0 addition, not present earlier
death_link = bool(info.get("death_link", False))
ctx.energy_link_increment = info.get("energy_link", 0)
ctx.custom_data_package = info.get("custom_data_package", 0)
logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}")
if ctx.energy_link_increment and ctx.ui:
ctx.ui.enable_energy_link()

150
Fill.py
View File

@@ -136,33 +136,98 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
itempool.extend(unplaced_items)
def remaining_fill(world: MultiWorld,
locations: typing.List[Location],
itempool: typing.List[Item]) -> None:
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
while locations and itempool:
item_to_place = itempool.pop()
spot_to_fill: typing.Optional[Location] = None
for i, location in enumerate(locations):
if location.item_rule(item_to_place):
# popping by index is faster than removing by content,
spot_to_fill = locations.pop(i)
# skipping a scan for the element
break
else:
# we filled all reachable spots.
# try swapping this item with previously placed items
for (i, location) in enumerate(placements):
placed_item = location.item
# Unplaceable items can sometimes be swapped infinitely. Limit the
# number of times we will swap an individual item to prevent this
if swapped_items[placed_item.player,
placed_item.name] > 1:
continue
location.item = None
placed_item.location = None
if location.item_rule(item_to_place):
# Add this item to the existing placement, and
# add the old item to the back of the queue
spot_to_fill = placements.pop(i)
swapped_items[placed_item.player,
placed_item.name] += 1
itempool.append(placed_item)
break
# Item can't be placed here, restore original item
location.item = placed_item
placed_item.location = location
if spot_to_fill is None:
# Can't place this item, move on to the next
unplaced_items.append(item_to_place)
continue
world.push_item(spot_to_fill, item_to_place, False)
placements.append(spot_to_fill)
if unplaced_items and locations:
# There are leftover unplaceable items and locations that won't accept them
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
itempool.extend(unplaced_items)
def fast_fill(world: MultiWorld,
item_pool: typing.List[Item],
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
placing = min(len(item_pool), len(fill_locations))
for item, location in zip(item_pool, fill_locations):
world.push_item(location, item, False)
return item_pool[placing:], fill_locations[placing:]
def distribute_items_restrictive(world: MultiWorld) -> None:
fill_locations = sorted(world.get_unfilled_locations())
world.random.shuffle(fill_locations)
# get items to distribute
itempool = sorted(world.itempool)
world.random.shuffle(itempool)
progitempool: typing.List[Item] = []
nonexcludeditempool: typing.List[Item] = []
localrestitempool: typing.Dict[int, typing.List[Item]] = {player: [] for player in range(1, world.players + 1)}
nonlocalrestitempool: typing.List[Item] = []
restitempool: typing.List[Item] = []
usefulitempool: typing.List[Item] = []
filleritempool: typing.List[Item] = []
for item in itempool:
if item.advancement:
progitempool.append(item)
elif item.useful: # this only gets nonprogression items which should not appear in excluded locations
nonexcludeditempool.append(item)
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].value:
nonlocalrestitempool.append(item)
elif item.useful:
usefulitempool.append(item)
else:
restitempool.append(item)
filleritempool.append(item)
call_all(world, "fill_hook", progitempool, nonexcludeditempool,
localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
loc_type: [] for loc_type in LocationProgressType}
@@ -184,50 +249,16 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
raise FillError(
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
if nonexcludeditempool:
world.random.shuffle(defaultlocations)
# needs logical fill to not conflict with local items
fill_restrictive(
world, world.state, defaultlocations, nonexcludeditempool)
if nonexcludeditempool:
raise FillError(
f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations')
remaining_fill(world, excludedlocations, filleritempool)
if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
defaultlocations = defaultlocations + excludedlocations
world.random.shuffle(defaultlocations)
restitempool = usefulitempool + filleritempool
if any(localrestitempool.values()): # we need to make sure some fills are limited to certain worlds
local_locations: typing.Dict[int, typing.List[Location]] = {player: [] for player in world.player_ids}
for location in defaultlocations:
local_locations[location.player].append(location)
for player_locations in local_locations.values():
world.random.shuffle(player_locations)
remaining_fill(world, defaultlocations, restitempool)
for player, items in localrestitempool.items(): # items already shuffled
player_local_locations = local_locations[player]
for item_to_place in items:
if not player_local_locations:
logging.warning(f"Ran out of local locations for player {player}, "
f"cannot place {item_to_place}.")
break
spot_to_fill = player_local_locations.pop()
world.push_item(spot_to_fill, item_to_place, False)
defaultlocations.remove(spot_to_fill)
for item_to_place in nonlocalrestitempool:
for i, location in enumerate(defaultlocations):
if location.player != item_to_place.player:
world.push_item(defaultlocations.pop(i), item_to_place, False)
break
else:
raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. "
f"Too many non-local items for too few remaining locations.")
world.random.shuffle(defaultlocations)
restitempool, defaultlocations = fast_fill(
world, restitempool, defaultlocations)
unplaced = progitempool + restitempool
unplaced = restitempool
unfilled = defaultlocations
if unplaced or unfilled:
@@ -241,15 +272,6 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
logging.info(f'Per-Player counts: {print_data})')
def fast_fill(world: MultiWorld,
item_pool: typing.List[Item],
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
placing = min(len(item_pool), len(fill_locations))
for item, location in zip(item_pool, fill_locations):
world.push_item(location, item, False)
return item_pool[placing:], fill_locations[placing:]
def flood_items(world: MultiWorld) -> None:
# get items to distribute
world.random.shuffle(world.itempool)

View File

@@ -23,7 +23,6 @@ from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from BaseClasses import seeddigits, get_seed
import Options
from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
import copy
@@ -337,19 +336,6 @@ def prefer_int(input_data: str) -> Union[str, int]:
return input_data
available_boss_names: Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
{'Agahnim', 'Agahnim2', 'Ganon'}}
available_boss_locations: Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in
Bosses.boss_location_table}
boss_shuffle_options = {None: 'none',
'none': 'none',
'basic': 'basic',
'full': 'full',
'chaos': 'chaos',
'singularity': 'singularity'
}
goals = {
'ganon': 'ganon',
'crystals': 'crystals',
@@ -456,42 +442,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
return weights
def get_plando_bosses(boss_shuffle: str, plando_options: Set[str]) -> str:
if boss_shuffle in boss_shuffle_options:
return boss_shuffle_options[boss_shuffle]
elif PlandoSettings.bosses in plando_options:
options = boss_shuffle.lower().split(";")
remainder_shuffle = "none" # vanilla
bosses = []
for boss in options:
if boss in boss_shuffle_options:
remainder_shuffle = boss_shuffle_options[boss]
elif "-" in boss:
loc, boss_name = boss.split("-")
if boss_name not in available_boss_names:
raise ValueError(f"Unknown Boss name {boss_name}")
if loc not in available_boss_locations:
raise ValueError(f"Unknown Boss Location {loc}")
level = ''
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
# split off level
loc = loc.split(" ")
level = f" {loc[-1]}"
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
if not Bosses.can_place_boss(boss_name.title(), loc, level):
raise ValueError(f"Cannot place {boss_name} at {loc}{level}")
bosses.append(boss)
elif boss not in available_boss_names:
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
else:
bosses.append(boss)
return ";".join(bosses + [remainder_shuffle])
else:
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)):
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoSettings):
if option_key in game_weights:
try:
if not option.supports_weighting:
@@ -502,10 +453,9 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
else:
if hasattr(player_option, "verify"):
player_option.verify(AutoWorldRegister.world_types[ret.game])
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
else:
setattr(ret, option_key, option(option.default))
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses):
@@ -549,11 +499,11 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
if ret.game in AutoWorldRegister.world_types:
for option_key, option in world_type.option_definitions.items():
handle_option(ret, game_weights, option_key, option)
handle_option(ret, game_weights, option_key, option, plando_options)
for option_key, option in Options.per_game_common_options.items():
# skip setting this option if already set from common_options, defaulting to root option
if not (option_key in Options.common_options and option_key not in game_weights):
handle_option(ret, game_weights, option_key, option)
handle_option(ret, game_weights, option_key, option, plando_options)
if PlandoSettings.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
@@ -636,8 +586,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.item_functionality = get_choice_legacy('item_functionality', weights)
boss_shuffle = get_choice_legacy('boss_shuffle', weights)
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
ret.enemy_damage = {None: 'default',
'default': 'default',

53
Main.py
View File

@@ -12,7 +12,7 @@ from typing import Dict, Tuple, Optional, Set
from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location
from worlds.alttp.Items import item_name_groups
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
from worlds.alttp.Regions import is_main_entrance
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from Utils import output_path, get_options, __version__, version_tuple
@@ -249,24 +249,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
output_file_futures.append(
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
def get_entrance_to_region(region: Region):
for entrance in region.entrances:
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic):
return entrance
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
return get_entrance_to_region(entrance.parent_region)
# collect ER hint info
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
world.shuffle[player] != "vanilla" or world.retro_caves[player]}
for region in world.regions:
if region.player in er_hint_data and region.locations:
main_entrance = get_entrance_to_region(region)
for location in region.locations:
if type(location.address) == int: # skips events and crystals
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][location.address] = main_entrance.name
er_hint_data: Dict[int, Dict[int, str]] = {}
AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)}
@@ -276,22 +261,23 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for location in world.get_filled_locations():
if type(location.address) is int:
main_entrance = get_entrance_to_region(location.parent_region)
if location.game != "A Link to the Past":
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
else:
main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
if location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
oldmancaves = []
@@ -305,7 +291,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
player = region.player
location_id = SHOP_ID_START + total_shop_slots + index
main_entrance = get_entrance_to_region(region)
main_entrance = region.get_connecting_entrance(is_main_entrance)
if main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[player]["Light World"].append(location_id)
else:
@@ -340,7 +326,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for player, world_precollected in world.precollected_items.items()}
precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
for slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data()

View File

@@ -126,6 +126,7 @@ class Context:
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
forced_auto_forfeits: typing.Dict[str, bool]
non_hintable_names: typing.Dict[str, typing.Set[str]]
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
@@ -196,7 +197,7 @@ class Context:
self.item_name_groups = {}
self.all_item_and_group_names = {}
self.forced_auto_forfeits = collections.defaultdict(lambda: False)
self.non_hintable_names = {}
self.non_hintable_names = collections.defaultdict(frozenset)
self._load_game_data()
self._init_game_data()
@@ -221,11 +222,11 @@ class Context:
self.all_item_and_group_names[game_name] = \
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
def item_names_for_game(self, game: str) -> typing.Dict[str, int]:
return self.gamespackage[game]["item_name_to_id"]
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None
def location_names_for_game(self, game: str) -> typing.Dict[str, int]:
return self.gamespackage[game]["location_name_to_id"]
def location_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
return self.gamespackage[game]["location_name_to_id"] if game in self.gamespackage else None
# General networking
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
@@ -743,6 +744,7 @@ async def countdown(ctx: Context, timer: int):
broadcast_countdown(ctx, 0, f"[Server]: GO")
ctx.countdown_timer = 0
def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}):
old_clients, new_clients = [], []
@@ -755,8 +757,10 @@ def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {})
ctx.broadcast(old_clients, [{"cmd": "Print", "text": text }])
ctx.broadcast(new_clients, [{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
def broadcast_countdown(ctx: Context, timer: int, message: str):
broadcast_text_all(ctx, message, { "type": "Countdown", "countdown": timer })
broadcast_text_all(ctx, message, {"type": "Countdown", "countdown": timer})
def get_players_string(ctx: Context):
auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth}
@@ -897,14 +901,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
ctx.save()
def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]:
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]:
hints = []
slots: typing.Set[int] = {slot}
for group_id, group in ctx.groups.items():
if slot in group:
slots.add(group_id)
seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name]
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
for finding_player, check_data in ctx.locations.items():
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
if receiving_player in slots and item_id == seeked_item_id:
@@ -1332,13 +1336,33 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
f"You have {points_available} points.")
return True
elif input_text.isnumeric():
game = self.ctx.games[self.client.slot]
hint_id = int(input_text)
hint_name = self.ctx.item_names[hint_id] \
if not for_location and hint_id in self.ctx.item_names \
else self.ctx.location_names[hint_id] \
if for_location and hint_id in self.ctx.location_names \
else None
if hint_name in self.ctx.non_hintable_names[game]:
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
hints = []
elif not for_location:
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id)
else:
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id)
else:
game = self.ctx.games[self.client.slot]
if game not in self.ctx.all_item_and_group_names:
self.output("Can't look up item/location for unknown game. Hint for ID instead.")
return False
names = self.ctx.location_names_for_game(game) \
if for_location else \
self.ctx.all_item_and_group_names[game]
hint_name, usable, response = get_intended_text(input_text,
names)
hint_name, usable, response = get_intended_text(input_text, names)
if usable:
if hint_name in self.ctx.non_hintable_names[game]:
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
@@ -1352,63 +1376,65 @@ class ClientMessageProcessor(CommonCommandProcessor):
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
else: # location name
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
cost = self.ctx.get_hint_cost(self.client.slot)
if hints:
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
old_hints = set(hints) - new_hints
if old_hints:
notify_hints(self.ctx, self.client.team, list(old_hints))
if not new_hints:
self.output("Hint was previously used, no points deducted.")
if new_hints:
found_hints = [hint for hint in new_hints if hint.found]
not_found_hints = [hint for hint in new_hints if not hint.found]
if not not_found_hints: # everything's been found, no need to pay
can_pay = 1000
elif cost:
can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call
else:
can_pay = 1000
self.ctx.random.shuffle(not_found_hints)
# By popular vote, make hints prefer non-local placements
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
hints = found_hints
while can_pay > 0:
if not not_found_hints:
break
hint = not_found_hints.pop()
hints.append(hint)
can_pay -= 1
self.ctx.hints_used[self.client.team, self.client.slot] += 1
points_available = get_client_points(self.ctx, self.client)
if not_found_hints:
if hints and cost and int((points_available // cost) == 0):
self.output(
f"There may be more hintables, however, you cannot afford to pay for any more. "
f" You have {points_available} and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
elif hints:
self.output(
"There may be more hintables, you can rerun the command to find more.")
else:
self.output(f"You can't afford the hint. "
f"You have {points_available} points and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
notify_hints(self.ctx, self.client.team, hints)
self.ctx.save()
return True
else:
self.output("Nothing found. Item/Location may not exist.")
return False
else:
self.output(response)
return False
if hints:
cost = self.ctx.get_hint_cost(self.client.slot)
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
old_hints = set(hints) - new_hints
if old_hints:
notify_hints(self.ctx, self.client.team, list(old_hints))
if not new_hints:
self.output("Hint was previously used, no points deducted.")
if new_hints:
found_hints = [hint for hint in new_hints if hint.found]
not_found_hints = [hint for hint in new_hints if not hint.found]
if not not_found_hints: # everything's been found, no need to pay
can_pay = 1000
elif cost:
can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call
else:
can_pay = 1000
self.ctx.random.shuffle(not_found_hints)
# By popular vote, make hints prefer non-local placements
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
hints = found_hints
while can_pay > 0:
if not not_found_hints:
break
hint = not_found_hints.pop()
hints.append(hint)
can_pay -= 1
self.ctx.hints_used[self.client.team, self.client.slot] += 1
points_available = get_client_points(self.ctx, self.client)
if not_found_hints:
if hints and cost and int((points_available // cost) == 0):
self.output(
f"There may be more hintables, however, you cannot afford to pay for any more. "
f" You have {points_available} and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
elif hints:
self.output(
"There may be more hintables, you can rerun the command to find more.")
else:
self.output(f"You can't afford the hint. "
f"You have {points_available} points and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
notify_hints(self.ctx, self.client.team, hints)
self.ctx.save()
return True
else:
self.output("Nothing found. Item/Location may not exist.")
return False
@mark_raw
def _cmd_hint(self, item_name: str = "") -> bool:
"""Use !hint {item_name},
@@ -1856,17 +1882,25 @@ class ServerCommandProcessor(CommonCommandProcessor):
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
item_name = " ".join(item_name)
game = self.ctx.games[slot]
item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game])
full_name = " ".join(item_name)
if full_name.isnumeric():
item, usable, response = int(full_name), True, None
elif game in self.ctx.all_item_and_group_names:
item, usable, response = get_intended_text(full_name, self.ctx.all_item_and_group_names[game])
else:
self.output("Can't look up item for unknown game. Hint for ID instead.")
return False
if usable:
if item_name in self.ctx.item_name_groups[game]:
if game in self.ctx.item_name_groups and item in self.ctx.item_name_groups[game]:
hints = []
for item_name_from_group in self.ctx.item_name_groups[game][item_name]:
for item_name_from_group in self.ctx.item_name_groups[game][item]:
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
else: # item name
hints = collect_hints(self.ctx, team, slot, item_name)
else: # item name or id
hints = collect_hints(self.ctx, team, slot, item)
if hints:
notify_hints(self.ctx, team, hints)
@@ -1887,11 +1921,22 @@ class ServerCommandProcessor(CommonCommandProcessor):
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
location_name = " ".join(location_name)
location_name, usable, response = get_intended_text(location_name,
self.ctx.location_names_for_game(self.ctx.games[slot]))
game = self.ctx.games[slot]
full_name = " ".join(location_name)
if full_name.isnumeric():
location, usable, response = int(full_name), True, None
elif self.ctx.location_names_for_game(game) is not None:
location, usable, response = get_intended_text(full_name, self.ctx.location_names_for_game(game))
else:
self.output("Can't look up location for unknown game. Hint for ID instead.")
return False
if usable:
hints = collect_hint_location_name(self.ctx, team, slot, location_name)
if isinstance(location, int):
hints = collect_hint_location_id(self.ctx, team, slot, location)
else:
hints = collect_hint_location_name(self.ctx, team, slot, location)
if hints:
notify_hints(self.ctx, team, hints)
else:
@@ -2041,15 +2086,28 @@ async def main(args: argparse.Namespace):
args.auto_shutdown, args.compatibility, args.log_network)
data_filename = args.multidata
try:
if not data_filename:
if not data_filename:
try:
filetypes = (("Multiworld data", (".archipelago", ".zip")),)
data_filename = Utils.open_filename("Select multiworld data", filetypes)
except Exception as e:
if isinstance(e, ImportError) or (e.__class__.__name__ == "TclError" and "no display" in str(e)):
if not isinstance(e, ImportError):
logging.error(f"Failed to load tkinter ({e})")
logging.info("Pass a multidata filename on command line to run headless.")
exit(1)
raise
if not data_filename:
logging.info("No file selected. Exiting.")
exit(1)
try:
ctx.load(data_filename, args.use_embedded_options)
except Exception as e:
logging.exception('Failed to read multiworld data (%s)' % e)
logging.exception(f"Failed to read multiworld data ({e})")
raise
ctx.init_save(not args.disable_save)

View File

@@ -26,15 +26,31 @@ class AssembleOptions(abc.ABCMeta):
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
options.update(new_options)
# apply aliases, without name_lookup
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("alias_")}
assert "random" not in aliases, "Choice option 'random' cannot be manually assigned."
# auto-alias Off and On being parsed as True and False
if "off" in options:
options["false"] = options["off"]
if "on" in options:
options["true"] = options["on"]
options.update(aliases)
if "verify" not in attrs:
# not overridden by class -> look up bases
verifiers = [f for f in (getattr(base, "verify", None) for base in bases) if f]
if len(verifiers) > 1: # verify multiple bases/mixins
def verify(self, *args, **kwargs) -> None:
for f in verifiers:
f(self, *args, **kwargs)
attrs["verify"] = verify
else:
assert verifiers, "class Option is supposed to implement def verify"
# auto-validate schema on __init__
if "schema" in attrs.keys():
@@ -112,6 +128,41 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
def from_any(cls, data: typing.Any) -> Option[T]:
raise NotImplementedError
if typing.TYPE_CHECKING:
from Generate import PlandoSettings
from worlds.AutoWorld import World
def verify(self, world: World, player_name: str, plando_options: PlandoSettings) -> None:
pass
else:
def verify(self, *args, **kwargs) -> None:
pass
class FreeText(Option):
"""Text option that allows users to enter strings.
Needs to be validated by the world or option definition."""
def __init__(self, value: str):
assert isinstance(value, str), "value of FreeText must be a string"
self.value = value
@property
def current_key(self) -> str:
return self.value
@classmethod
def from_text(cls, text: str) -> FreeText:
return cls(text)
@classmethod
def from_any(cls, data: typing.Any) -> FreeText:
return cls.from_text(str(data))
@classmethod
def get_option_name(cls, value: T) -> str:
return value
class NumericOption(Option[int], numbers.Integral):
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
@@ -368,6 +419,53 @@ class Choice(NumericOption):
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
class TextChoice(Choice):
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
def __init__(self, value: typing.Union[str, int]):
assert isinstance(value, str) or isinstance(value, int), \
f"{value} is not a valid option for {self.__class__.__name__}"
self.value = value
super(TextChoice, self).__init__()
@property
def current_key(self) -> str:
if isinstance(self.value, str):
return self.value
else:
return self.name_lookup[self.value]
@classmethod
def from_text(cls, text: str) -> TextChoice:
if text.lower() == "random": # chooses a random defined option but won't use any free text options
return cls(random.choice(list(cls.name_lookup)))
for option_name, value in cls.options.items():
if option_name.lower() == text.lower():
return cls(value)
return cls(text)
@classmethod
def get_option_name(cls, value: T) -> str:
if isinstance(value, str):
return value
return cls.name_lookup[value]
def __eq__(self, other: typing.Any):
if isinstance(other, self.__class__):
return other.value == self.value
elif isinstance(other, str):
if other in self.options:
return other == self.current_key
return other == self.value
elif isinstance(other, int):
assert other in self.name_lookup, f"compared against an int that could never be equal. {self} == {other}"
return other == self.value
elif isinstance(other, bool):
return other == bool(self.value)
else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
class Range(NumericOption):
range_start = 0
range_end = 1
@@ -385,7 +483,7 @@ class Range(NumericOption):
if text.startswith("random"):
return cls.weighted_range(text)
elif text == "default" and hasattr(cls, "default"):
return cls(cls.default)
return cls.from_any(cls.default)
elif text == "high":
return cls(cls.range_end)
elif text == "low":
@@ -396,7 +494,7 @@ class Range(NumericOption):
and text in ("true", "false"):
# these are the conditions where "true" and "false" make sense
if text == "true":
return cls(cls.default)
return cls.from_any(cls.default)
else: # "false"
return cls(0)
return cls(int(text))
@@ -507,7 +605,7 @@ class VerifyKeys:
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
f"Allowed keys: {cls.valid_keys}.")
def verify(self, world):
def verify(self, world, player_name: str, plando_options) -> None:
if self.convert_name_groups and self.verify_item_name:
new_value = type(self.value)() # empty container of whatever value is
for item_name in self.value:
@@ -600,10 +698,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
@classmethod
def from_any(cls, data: typing.Any):
if type(data) == list:
cls.verify_keys(data)
return cls(data)
elif type(data) == set:
if isinstance(data, (list, set, frozenset)):
cls.verify_keys(data)
return cls(data)
return cls.from_text(str(data))
@@ -732,8 +827,8 @@ class ItemLinks(OptionList):
pool |= {item_name}
return pool
def verify(self, world):
super(ItemLinks, self).verify(world)
def verify(self, world, player_name: str, plando_options) -> None:
super(ItemLinks, self).verify(world, player_name, plando_options)
existing_links = set()
for link in self.value:
if link["name"] in existing_links:

View File

@@ -15,9 +15,6 @@ import typing
from json import loads, dumps
import ModuleUpdate
ModuleUpdate.update()
from Utils import init_logging, messagebox
if __name__ == "__main__":

View File

@@ -19,10 +19,11 @@ from sc2.data import Race
from sc2.main import run_game
from sc2.player import Bot
import NetUtils
from MultiServer import mark_raw
from Utils import init_logging, is_windows
from worlds.sc2wol import SC2WoLWorld
from worlds.sc2wol.Items import lookup_id_to_name, item_table
from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
from worlds.sc2wol.MissionTables import lookup_id_to_mission
from worlds.sc2wol.Regions import MissionInfo
@@ -135,7 +136,7 @@ class SC2Context(CommonContext):
last_loc_list = None
difficulty_override = -1
mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
raw_text_parser: RawJSONtoTextParser
last_bot: typing.Optional[ArchipelagoBot] = None
def __init__(self, *args, **kwargs):
super(SC2Context, self).__init__(*args, **kwargs)
@@ -164,10 +165,13 @@ class SC2Context(CommonContext):
check_mod_install()
def on_print_json(self, args: dict):
# goes to this world
if "receiving" in args and self.slot_concerns_self(args["receiving"]):
relevant = True
# found in this world
elif "item" in args and self.slot_concerns_self(args["item"].player):
relevant = True
# not related
else:
relevant = False
@@ -291,34 +295,37 @@ class SC2Context(CommonContext):
category_panel.add_widget(
Label(text=category, size_hint_y=None, height=50, outline_width=1))
# Map is completed
for mission in categories[category]:
text = mission
tooltip = ""
text: str = mission
tooltip: str = ""
# Map has uncollected locations
if mission in unfinished_missions:
text = f"[color=6495ED]{text}[/color]"
tooltip = f"Uncollected locations:\n"
tooltip += "\n".join([self.ctx.location_names[loc] for loc in
self.ctx.locations_for_mission(mission)
if loc in self.ctx.missing_locations])
elif mission in available_missions:
text = f"[color=FFFFFF]{text}[/color]"
# Map requirements not met
else:
text = f"[color=a9a9a9]{text}[/color]"
tooltip = f"Requires: "
if len(self.ctx.mission_req_table[mission].required_world) > 0:
if self.ctx.mission_req_table[mission].required_world:
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
req_mission in
self.ctx.mission_req_table[mission].required_world)
if self.ctx.mission_req_table[mission].number > 0:
if self.ctx.mission_req_table[mission].number:
tooltip += " and "
if self.ctx.mission_req_table[mission].number > 0:
if self.ctx.mission_req_table[mission].number:
tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed"
remaining_location_names: typing.List[str] = [
self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
if loc in self.ctx.missing_locations]
if remaining_location_names:
if tooltip:
tooltip += "\n"
tooltip += f"Uncollected locations:\n"
tooltip += "\n".join(remaining_location_names)
mission_button = MissionButton(text=text, size_hint_y=None, height=50)
mission_button.tooltip_text = tooltip
@@ -355,6 +362,8 @@ class SC2Context(CommonContext):
async def shutdown(self):
await super(SC2Context, self).shutdown()
if self.last_bot:
self.last_bot.want_close = True
if self.sc2_run_task:
self.sc2_run_task.cancel()
@@ -431,47 +440,27 @@ wol_default_categories = [
]
def calculate_items(items):
unit_unlocks = 0
armory1_unlocks = 0
armory2_unlocks = 0
upgrade_unlocks = 0
building_unlocks = 0
merc_unlocks = 0
lab_unlocks = 0
protoss_unlock = 0
minerals = 0
vespene = 0
supply = 0
def calculate_items(items: typing.List[NetUtils.NetworkItem]) -> typing.List[int]:
network_item: NetUtils.NetworkItem
accumulators: typing.List[int] = [0 for _ in type_flaggroups]
for item in items:
data = lookup_id_to_name[item.item]
for network_item in items:
name: str = lookup_id_to_name[network_item.item]
item_data: ItemData = item_table[name]
if item_table[data].type == "Unit":
unit_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Upgrade":
upgrade_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Armory 1":
armory1_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Armory 2":
armory2_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Building":
building_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Mercenary":
merc_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Laboratory":
lab_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Protoss":
protoss_unlock += (1 << item_table[data].number)
elif item_table[data].type == "Minerals":
minerals += item_table[data].number
elif item_table[data].type == "Vespene":
vespene += item_table[data].number
elif item_table[data].type == "Supply":
supply += item_table[data].number
# exists exactly once
if item_data.quantity == 1:
accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number
return [unit_unlocks, upgrade_unlocks, armory1_unlocks, armory2_unlocks, building_unlocks, merc_unlocks,
lab_unlocks, protoss_unlock, minerals, vespene, supply]
# exists multiple times
elif item_data.type == "Upgrade":
accumulators[type_flaggroups[item_data.type]] += 1 << item_data.number
# sum
else:
accumulators[type_flaggroups[item_data.type]] += item_data.number
return accumulators
def calc_difficulty(difficulty):
@@ -502,7 +491,7 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
setup_done: bool
ctx: SC2Context
mission_id: int
want_close: bool = False
can_read_game = False
last_received_update: int = 0
@@ -510,12 +499,17 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
def __init__(self, ctx: SC2Context, mission_id):
self.setup_done = False
self.ctx = ctx
self.ctx.last_bot = self
self.mission_id = mission_id
self.boni = [False for _ in range(max_bonus)]
super(ArchipelagoBot, self).__init__()
async def on_step(self, iteration: int):
if self.want_close:
self.want_close = False
await self._client.leave()
return
game_state = 0
if not self.setup_done:
self.setup_done = True
@@ -799,7 +793,12 @@ def check_game_install_path() -> bool:
with open(einfo) as f:
content = f.read()
if content:
base = re.search(r" = (.*)Versions", content).group(1)
try:
base = re.search(r" = (.*)Versions", content).group(1)
except AttributeError:
sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then "
f"try again.")
return False
if os.path.exists(base):
executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions")
@@ -816,7 +815,8 @@ def check_game_install_path() -> bool:
else:
sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
else:
sc2_logger.warning(f"Couldn't find {einfo}. Please run /set_path with your SC2 install directory.")
sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. "
f"If that fails, please run /set_path with your SC2 install directory.")
return False

View File

@@ -1,15 +1,16 @@
from __future__ import annotations
import functools
import websockets
import asyncio
import collections
import datetime
import functools
import logging
import pickle
import random
import socket
import threading
import time
import random
import pickle
import logging
import datetime
import websockets
import Utils
from .models import db_session, Room, select, commit, Command, db
@@ -49,6 +50,8 @@ class DBCommandProcessor(ServerCommandProcessor):
class WebHostContext(Context):
room_id: int
def __init__(self, static_server_data: dict):
# static server data is used during _load_game_data to load required data,
# without needing to import worlds system, which takes quite a bit of memory
@@ -62,6 +65,8 @@ class WebHostContext(Context):
def _load_game_data(self):
for key, value in self.static_server_data.items():
setattr(self, key, value)
self.forced_auto_forfeits = collections.defaultdict(lambda: False, self.forced_auto_forfeits)
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
def listen_to_db_commands(self):
cmdprocessor = DBCommandProcessor(self)

343
docs/adding games.md Normal file
View File

@@ -0,0 +1,343 @@
# 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 two key steps to incorporating a game into Archipelago:
- Game Modification
- Archipelago Server Integration
Refer to the following documents as well:
- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) for network communication between client and server.
- [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md) for documentation on server side code and creating a world package.
# 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.
### Patches
#### IPS
IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
fine.
#### UPS, BPS, VCDIFF (xdelta), bsdiff
Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
compression, so this format is used by APBP.
Only a bsdiff module is integrated into AP. If the final patch requires or is based on any other patch, convert them to
bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
#### APBP Archipelago Binary Patch
Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
bsdiff between the original and the randomized ROM.
To make using APBP easy, they can be generated by inheriting from `Patch.APDeltaPatch`.
### 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.
They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
generated per seed.
If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
integration into the Webhost by inheriting from `Patch.APContainer`.
## 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)
The minimum requirements for a new archipelago world are the package itself (the world folder containing a file named `__init__.py`),
which must define a `World` class object for the game with a game name, create an equal number of items and locations with rules,
a win condition, and at least one `Region` object.
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]` and
[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md),
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.

View File

@@ -23,3 +23,10 @@ No metadata is specified yet.
## Extra Data
The zip can contain arbitrary files in addition what was specified above.
## Caveats
Imports from other files inside the apworld have to use relative imports.
Imports from AP base have to use absolute imports, e.g. Options.py and worlds/AutoWorld.py.

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 209 KiB

View File

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 216 KiB

View File

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

View File

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

View File

Before

Width:  |  Height:  |  Size: 257 KiB

After

Width:  |  Height:  |  Size: 257 KiB

View File

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 221 KiB

View File

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 245 KiB

View File

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -1,8 +1,4 @@
# Archipelago Network Diagram
(Psst, scroll down and zoom in.)
```{mermaid}
```mermaid
flowchart LR
%% Diagram arranged specifically so output generates no terrible crossing lines.
%% AP Server
@@ -73,6 +69,12 @@ flowchart LR
end
SNI <-- Various, depending on SNES device --> SMZ
%% Donkey Kong Country 3
subgraph Donkey Kong Country 3
DK3[SNES]
end
SNI <-- Various, depending on SNES device --> DK3
%% Native Clients or Games
%% Games or clients which compile to native or which the client is integrated in the game.
subgraph "Native"
@@ -86,10 +88,12 @@ flowchart LR
MT[Meritous]
TW[The Witness]
SA2B[Sonic Adventure 2: Battle]
DS3[Dark Souls 3]
APCLIENTPP <--> SOE
APCLIENTPP <--> MT
APCLIENTPP <-- The Witness Randomizer --> TW
APCLIENTPP <--> DS3
APCPP <--> SM64
APCPP <--> V6
APCPP <--> SA2B

View File

@@ -1,20 +1,17 @@
# Archipelago Network Protocol
# Archipelago General Client
## Archipelago Connection Handshake
These steps should be followed in order to establish a gameplay connection with an Archipelago session.
1. Client establishes WebSocket connection to Archipelago server.
2. Server accepts connection and responds with a [RoomInfo](#roominfo) packet.
3. Client may send a [GetDataPackage](#getdatapackage) packet.
4. Server sends a [DataPackage](#datapackage) packet in return. (If the client sent GetDataPackage.)
5. Client sends [Connect](#connect) packet in order to authenticate with the server.
6. Server validates the client's packet and responds with [Connected](#connected) or
[ConnectionRefused](#connectionrefused).
7. Server may send [ReceivedItems](#receiveditems) to the client, in the case that the client is missing items that
are queued up for it.
8. Server sends [Print](#print) to all players to notify them of the new client connection.
2. Server accepts connection and responds with a [RoomInfo](#RoomInfo) packet.
3. Client may send a [GetDataPackage](#GetDataPackage) packet.
4. Server sends a [DataPackage](#DataPackage) packet in return. (If the client sent GetDataPackage.)
5. Client sends [Connect](#Connect) packet in order to authenticate with the server.
6. Server validates the client's packet and responds with [Connected](#Connected) or [ConnectionRefused](#ConnectionRefused).
7. Server may send [ReceivedItems](#ReceivedItems) to the client, in the case that the client is missing items that are queued up for it.
8. Server sends [Print](#Print) to all players to notify them of the new client connection.
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#connectionrefused) then
the server will maintain the connection and allow for follow-up [Connect](#connect) packet.
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet.
There are also a number of community-supported libraries available that implement this network protocol to make integrating with Archipelago easier.
@@ -30,26 +27,18 @@ There are also a number of community-supported libraries available that implemen
| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | |
## Synchronizing Items
When the client receives a [ReceivedItems](#receiveditems) packet, if the `index` argument does not match the next index
that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished
by sending the server a [Sync](#sync) packet and then a [LocationChecks](#locationchecks) packet.
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.
Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay
interruption.
Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay interruption.
When the client receives a [ReceivedItems](#receiveditems) packet and the `index` arg is `0` (zero) then the client
should accept the provided `items` list as its full inventory. (Abandon previous inventory.)
When the client receives a [ReceivedItems](#ReceivedItems) packet and the `index` arg is `0` (zero) then the client should accept the provided `items` list as its full inventory. (Abandon previous inventory.)
## Archipelago Protocol Packets
Packets are sent between the multiworld server and client in order to sync information between them.
Below is a directory of each packet.
# Archipelago Protocol Packets
Packets are sent between the multiworld server and client in order to sync information between them. Below is a directory of each packet.
Packets are simple JSON lists in which any number of ordered network commands can be sent, which are objects.
Each command has a "cmd" key, indicating its purpose. All packet argument types documented here refer to JSON types,
unless otherwise specified.
Packets are simple JSON lists in which any number of ordered network commands can be sent, which are objects. Each command has a "cmd" key, indicating its purpose. All packet argument types documented here refer to JSON types, unless otherwise specified.
An object can contain the "class" key, which will tell the content data type, such as "Version" in the following
example.
An object can contain the "class" key, which will tell the content data type, such as "Version" in the following example.
Example:
```javascript
@@ -57,41 +46,40 @@ Example:
```
## (Server -> Client)
These packets are sent from the multiworld server to the client. They are not messages which the server accepts.
* [RoomInfo](#roominfo)
* [ConnectionRefused](#connectionrefused)
* [Connected](#connected)
* [ReceivedItems](#receiveditems)
* [LocationInfo](#locationinfo)
* [RoomUpdate](#roomupdate)
* [Print](#print)
* [PrintJSON](#printjson)
* [DataPackage](#datapackage)
* [Bounced](#bounced)
* [InvalidPacket](#invalidpacket)
* [Retrieved](#retrieved)
* [SetReply](#setreply)
These packets are are sent from the multiworld server to the client. They are not messages which the server accepts.
* [RoomInfo](#RoomInfo)
* [ConnectionRefused](#ConnectionRefused)
* [Connected](#Connected)
* [ReceivedItems](#ReceivedItems)
* [LocationInfo](#LocationInfo)
* [RoomUpdate](#RoomUpdate)
* [Print](#Print)
* [PrintJSON](#PrintJSON)
* [DataPackage](#DataPackage)
* [Bounced](#Bounced)
* [InvalidPacket](#InvalidPacket)
* [Retrieved](#Retrieved)
* [SetReply](#SetReply)
### RoomInfo
Sent to clients when they connect to an Archipelago server.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| version | [NetworkVersion](#networkversion) | Object denoting the version of Archipelago which the server is running. |
| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
| password | bool | Denoted whether a password is required to join this room.|
| permissions | dict\[str, [Permission](#permission)\[int\]\] | Mapping of permission name to [Permission](#permission), keys are: "forfeit", "collect" and "remaining". |
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" and "remaining". |
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
| games | list\[str\] | List of games present in this multiworld. |
| datapackage_version | int | Sum of individual games' datapackage version. Deprecated. Use `datapackage_versions` instead. |
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#data-package-contents). |
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). |
| seed_name | str | uniquely identifying name of this generation |
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
#### forfeit
Dictates what is allowed when it comes to a player forfeiting their run. A forfeit is an action which distributes the
rest of the items in a player's run to those other players awaiting them.
Dictates what is allowed when it comes to a player forfeiting their run. A forfeit is an action which distributes the rest of the items in a player's run to those other players awaiting them.
* `auto`: Distributes a player's items to other players when they complete their goal.
* `enabled`: Denotes that players may forfeit at any time in the game.
@@ -100,8 +88,7 @@ rest of the items in a player's run to those other players awaiting them.
* `goal`: Allows for manual use of forfeit command once a player completes their goal. (Disabled until goal completion)
#### collect
Dictates what is allowed when it comes to a player collecting their run. A collect is an action which sends the rest of
the items in a player's run.
Dictates what is allowed when it comes to a player collecting their run. A collect is an action which sends the rest of the items in a player's run.
* `auto`: Automatically when they complete their goal.
* `enabled`: Denotes that players may !collect at any time in the game.
@@ -135,13 +122,13 @@ Sent to clients when the connection handshake is successfully completed.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| team | int | Your team number. See [NetworkPlayer](#networkplayer) for more info on team number. |
| slot | int | Your slot number on your team. See [NetworkPlayer](#networkplayer) for more info on the slot number. |
| players | list\[[NetworkPlayer](#networkplayer)\] | List denoting other players in the multiworld, whether connected or not. |
| team | int | Your team number. See [NetworkPlayer](#NetworkPlayer) for more info on team number. |
| slot | int | Your slot number on your team. See [NetworkPlayer](#NetworkPlayer) for more info on the slot number. |
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | List denoting other players in the multiworld, whether connected or not. |
| missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. |
| checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. Location ids are in the range of ± 2<sup>53</sup>-1. |
| slot_data | dict | Contains a json object for slot related data, differs per game. Empty if not required. |
| slot_info | dict\[int, [NetworkSlot](#networkslot)\] | maps each slot to a [NetworkSlot](#networkslot) information |
| slot_info | dict\[int, [NetworkSlot](#NetworkSlot)\] | maps each slot to a [NetworkSlot](#NetworkSlot) information |
### ReceivedItems
Sent to clients when they receive an item.
@@ -149,25 +136,25 @@ Sent to clients when they receive an item.
| Name | Type | Notes |
| ---- | ---- | ----- |
| index | int | The next empty slot in the list of items for the receiving client. |
| items | list\[[NetworkItem](#networkitem)\] | The items which the client is receiving. |
| items | list\[[NetworkItem](#NetworkItem)\] | The items which the client is receiving. |
### LocationInfo
Sent to clients to acknowledge a received [LocationScouts](#locationscouts) packet and responds with the item in the location(s) being scouted.
Sent to clients to acknowledge a received [LocationScouts](#LocationScouts) packet and responds with the item in the location(s) being scouted.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| locations | list\[[NetworkItem](#networkitem)\] | Contains list of item(s) in the location(s) scouted. |
| locations | list\[[NetworkItem](#NetworkItem)\] | Contains list of item(s) in the location(s) scouted. |
### RoomUpdate
Sent when there is a need to update information about the present game session. Generally useful for async games.
Once authenticated (received Connected), this may also contain data from Connected.
#### Arguments
The arguments for RoomUpdate are identical to [RoomInfo](#roominfo) barring:
The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring:
| Name | Type | Notes |
| ---- | ---- | ----- |
| hint_points | int | New argument. The client's current hint points. |
| players | list\[[NetworkPlayer](#networkplayer)\] | Send in the event of an alias rename. Always sends all players, whether connected or not. |
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Send in the event of an alias rename. Always sends all players, whether connected or not. |
| checked_locations | list\[int\] | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. |
| missing_locations | list\[int\] | Should never be sent as an update, if needed is the inverse of checked_locations. |
@@ -182,15 +169,14 @@ Sent to clients purely to display a message to the player.
| text | str | Message to display to player. |
### PrintJSON
Sent to clients purely to display a message to the player. This packet differs from [Print](#print) in that the data
being sent with this packet allows for more configurable or specific messaging.
Sent to clients purely to display a message to the player. This packet differs from [Print](#Print) in that the data being sent with this packet allows for more configurable or specific messaging.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. |
| type | str | May be present to indicate the [PrintJsonType](#PrintJsonType) of this message. |
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. |
| item | [NetworkItem](#networkitem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. |
| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. |
| found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. |
| countdown | int | Is present if type is `Countdown`, denotes the amount of seconds remaining on the countdown. |
@@ -205,37 +191,35 @@ Currently defined types are:
| Countdown | The message contains information about the current server Countdown. |
### DataPackage
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most
easily communicate with the Archipelago server. Contents include things like location id to name mappings,
among others; see [Data Package Contents](#data-package-contents) for more info.
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| data | [DataPackageObject](#data-package-contents) | The data package as a JSON object. |
| data | [DataPackageObject](#Data-Package-Contents) | The data package as a JSON object. |
### Bounced
Sent to clients after a client requested this message be sent to them, more info in the [Bounce](#bounce) package.
Sent to clients after a client requested this message be sent to them, more info in the [Bounce](#Bounce) package.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| games | list\[str\] | Optional. Game names this message is targeting |
| slots | list\[int\] | Optional. Player slot IDs that this message is targeting |
| tags | list\[str\] | Optional. Client [Tags](#tags) this message is targeting |
| data | dict | The data in the [Bounce](#bounce) package copied |
| tags | list\[str\] | Optional. Client [Tags](#Tags) this message is targeting |
| data | dict | The data in the [Bounce](#Bounce) package copied |
### InvalidPacket
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
#### Arguments
| Name | Type | Notes |
|--------------|---------------|-------------------------------------------------------------------------------------------|
| type | str | The [PacketProblemType](#packetproblemtype) that was detected in the packet. |
| Name | Type | Notes |
| ---- | ---- | ----- |
| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. |
| original_cmd | Optional[str] | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. |
| text | str | A descriptive message of the problem at hand. |
| text | str | A descriptive message of the problem at hand. |
#### PacketProblemType
##### PacketProblemType
`PacketProblemType` indicates the type of problem that was detected in the faulty packet, the known problem types are below but others may be added in the future.
| Type | Notes |
@@ -244,19 +228,16 @@ Sent to clients if the server caught a problem with a packet. This only occurs f
| arguments | Arguments of the faulty packet which were not correct. |
### Retrieved
Sent to clients as a response the a [Get](#get) package
Sent to clients as a response the a [Get](#Get) package.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| keys | dict\[str\, any] | A key-value collection containing all the values for the keys requested in the [Get](#get) package. |
| keys | dict\[str\, any] | A key-value collection containing all the values for the keys requested in the [Get](#Get) package. |
Additional arguments added to the [Get](#get) package that triggered this [Retrieved](#retrieved) will also be passed
along.
Additional arguments added to the [Get](#Get) package that triggered this [Retrieved](#Retrieved) will also be passed along.
### SetReply
Sent to clients in response to a [Set](#set) package if want_reply was set to true, or if the client has registered to
receive updates for a certain key using the [SetNotify](#setnotify) package. SetReply packages are sent even if a
[Set](#set) package did not alter the value for the key.
Sent to clients in response to a [Set](#Set) package if want_reply was set to true, or if the client has registered to receive updates for a certain key using the [SetNotify](#SetNotify) package. SetReply packages are sent even if a [Set](#Set) package did not alter the value for the key.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
@@ -264,23 +245,22 @@ receive updates for a certain key using the [SetNotify](#setnotify) package. Set
| value | any | The new value for the key. |
| original_value | any | The value the key had before it was updated. |
Additional arguments added to the [Set](#set) package that triggered this [SetReply](#setreply) will also be passed
along.
Additional arguments added to the [Set](#Set) package that triggered this [SetReply](#SetReply) will also be passed along.
## (Client -> Server)
These packets are sent purely from client to server. They are not accepted by clients.
* [Connect](#connect)
* [Sync](#sync)
* [LocationChecks](#locationchecks)
* [LocationScouts](#locationscouts)
* [StatusUpdate](#statusupdate)
* [Say](#say)
* [GetDataPackage](#getdatapackage)
* [Bounce](#bounce)
* [Get](#get)
* [Set](#set)
* [SetNotify](#setnotify)
* [Connect](#Connect)
* [Sync](#Sync)
* [LocationChecks](#LocationChecks)
* [LocationScouts](#LocationScouts)
* [StatusUpdate](#StatusUpdate)
* [Say](#Say)
* [GetDataPackage](#GetDataPackage)
* [Bounce](#Bounce)
* [Get](#Get)
* [Set](#Set)
* [SetNotify](#SetNotify)
### Connect
Sent by the client to initiate a connection to an Archipelago game session.
@@ -292,9 +272,9 @@ Sent by the client to initiate a connection to an Archipelago game session.
| game | str | The name of the game the client is playing. Example: `A Link to the Past` |
| name | str | The player name for this client. |
| uuid | str | Unique identifier for player client. |
| version | [NetworkVersion](#networkversion) | An object representing the Archipelago version this client supports. |
| version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. |
| items_handling | int | Flags configuring which items should be sent by the server. Read below for individual flags. |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#tags) |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
#### items_handling flags
| Value | Meaning |
@@ -306,8 +286,7 @@ Sent by the client to initiate a connection to an Archipelago game session.
| null | Null or undefined loads settings from world definition for backwards compatibility. This is deprecated. |
#### Authentication
Many, if not all, other packets require a successfully authenticated client. This is described in more detail in
[Archipelago Connection Handshake](#archipelago-connection-handshake).
Many, if not all, other packets require a successfully authenticated client. This is described in more detail in [Archipelago Connection Handshake](#Archipelago-Connection-Handshake).
### ConnectUpdate
Update arguments from the Connect package, currently only updating tags and items_handling is supported.
@@ -316,16 +295,15 @@ Update arguments from the Connect package, currently only updating tags and item
| Name | Type | Notes |
| ---- | ---- | ----- |
| items_handling | int | Flags configuring which items should be sent by the server. |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#tags) |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
### Sync
Sent to server to request a [ReceivedItems](#receiveditems) packet to synchronize items.
Sent to server to request a [ReceivedItems](#ReceivedItems) packet to synchronize items.
#### Arguments
No arguments necessary.
### LocationChecks
Sent to server to inform it of locations that the client has checked. Used to inform the server of new checks that are
made, as well as to sync state.
Sent to server to inform it of locations that the client has checked. Used to inform the server of new checks that are made, as well as to sync state.
#### Arguments
| Name | Type | Notes |
@@ -333,15 +311,13 @@ made, as well as to sync state.
| locations | list\[int\] | The ids of the locations checked by the client. May contain any number of checks, even ones sent before; duplicates do not cause issues with the Archipelago server. |
### LocationScouts
Sent to the server to inform it of locations the client has seen, but not checked. Useful in cases in which the item may
appear in the game world, such as 'ledge items' in A Link to the Past. The server will always respond with a
[LocationInfo](#locationinfo) packet with the items located in the scouted location.
Sent to the server to inform it of locations the client has seen, but not checked. Useful in cases in which the item may appear in the game world, such as 'ledge items' in A Link to the Past. The server will always respond with a [LocationInfo](#LocationInfo) packet with the items located in the scouted location.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. |
| create_as_hint | int | If non-zero, the scouted locations get created and broadcast as a player-visible hint. <br/>If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. |
| create_as_hint | int | If non-zero, the scouted locations get created and broadcasted as a player-visible hint. <br/>If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. |
### StatusUpdate
Sent to the server to update on the sender's status. Examples include readiness or goal completion. (Example: defeated Ganon in A Link to the Past)
@@ -349,7 +325,7 @@ Sent to the server to update on the sender's status. Examples include readiness
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| status | ClientStatus\[int\] | One of [Client States](#client-states). Send as int. Follow the link for more information. |
| status | ClientStatus\[int\] | One of [Client States](#Client-States). Send as int. Follow the link for more information. |
### Say
Basic chat command which sends text to the server to be distributed to other clients.
@@ -363,8 +339,8 @@ Basic chat command which sends text to the server to be distributed to other cli
Requests the data package from the server. Does not require client authentication.
#### Arguments
| Name | Type | Notes |
|-------| ----- | ---- |
| Name | Type | Notes |
|-------| ----- |---------------------------------------------------------------------------------------------------------------------------------|
| games | list\[str\] | Optional. If specified, will only send back the specified data. Such as, \["Factorio"\] -> Datapackage with only Factorio data. |
### Bounce
@@ -380,36 +356,29 @@ the server will forward the message to all those targets to which any one requir
| data | dict | Any data you want to send |
### Get
Used to request a single or multiple values from the server's data storage, see the [Set](#set) package for how to write
values to the data storage. A Get package will be answered with a [Retrieved](#retrieved) package.
Used to request a single or multiple values from the server's data storage, see the [Set](#Set) package for how to write values to the data storage. A Get package will be answered with a [Retrieved](#Retrieved) package.
#### Arguments
| Name | Type | Notes |
| ------ | ----- | ------ |
| keys | list\[str\] | Keys to retrieve the values for. |
Additional arguments sent in this package will also be added to the [Retrieved](#retrieved) package it triggers.
Additional arguments sent in this package will also be added to the [Retrieved](#Retrieved) package it triggers.
### Set
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later.
Values for keys in the data storage can be retrieved with a [Get](#get) package, or monitored with a
[SetNotify](#setnotify) package.
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.
#### Arguments
| Name | Type | Notes |
| ------ | ----- | ------ |
| key | str | The key to manipulate. |
| default | any | The default value to use in case the key has no value on the server. |
| want_reply | bool | If set, the server will send a [SetReply](#setreply) response back to the client. |
| operations | list\[[DataStorageOperation](#datastorageoperation)\] | Operations to apply to the value, multiple operations can be present and they will be executed in order of appearance. |
| want_reply | bool | If set, the server will send a [SetReply](#SetReply) response back to the client. |
| operations | list\[[DataStorageOperation](#DataStorageOperation)\] | Operations to apply to the value, multiple operations can be present and they will be executed in order of appearance. |
Additional arguments sent in this package will also be added to the [SetReply](#setreply) package it triggers.
Additional arguments sent in this package will also be added to the [SetReply](#SetReply) package it triggers.
#### DataStorageOperation
A DataStorageOperation manipulates or alters the value of a key in the data storage. If the operation transforms the
value from one state to another then the current value of the key is used as the starting point otherwise the
[Set](#set)'s package `default` is used if the key does not exist on the server already.
DataStorageOperations consist of an object containing both the operation to be applied, provided in the form of a
string, as well as the value to be used for that operation, Example:
A DataStorageOperation manipulates or alters the value of a key in the data storage. If the operation transforms the value from one state to another then the current value of the key is used as the starting point otherwise the [Set](#Set)'s package `default` is used if the key does not exist on the server already.
DataStorageOperations consist of an object containing both the operation to be applied, provided in the form of a string, as well as the value to be used for that operation, Example:
```json
{"operation": "add", "value": 12}
```
@@ -418,7 +387,7 @@ The following operations can be applied to a datastorage key
| Operation | Effect |
| ------ | ----- |
| replace | Sets the current value of the key to `value`. |
| default | If the key has no value yet, sets the current value of the key to `default` of the [Set](#set)'s package (`value` is ignored). |
| default | If the key has no value yet, sets the current value of the key to `default` of the [Set](#Set)'s package (`value` is ignored). |
| add | Adds `value` to the current value of the key, if both the current value and `value` are arrays then `value` will be appended to the current value. |
| mul | Multiplies the current value of the key by `value`. |
| pow | Multiplies the current value of the key to the power of `value`. |
@@ -432,35 +401,28 @@ The following operations can be applied to a datastorage key
| right_shift | Applies a bitwise right-shift to the current value of the key by `value`. |
### SetNotify
Used to register your current session for receiving all [SetReply](#setreply) packages of certain keys to allow your client to keep track of changes.
Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes.
#### Arguments
| Name | Type | Notes |
| ------ | ----- | ------ |
| keys | list\[str\] | Keys to receive all [SetReply](#setreply) packages for. |
| keys | list\[str\] | Keys to receive all [SetReply](#SetReply) packages for. |
## Appendix
### Coop
Coop in Archipelago is automatically facilitated by the server, however some default behaviour may not be what you
desire.
Coop in Archipelago is automatically facilitated by the server, however some of the default behaviour may not be what you desire.
If the game in question is a remote-items game (attribute on AutoWorld), then all items will always be sent and received.
If the game in question is not a remote-items game, then any items that are placed within the same world will not be
sent by the server.
If the game in question is not a remote-items game, then any items that are placed within the same world will not be send by the server.
To manually react to others in the same player slot doing checks, listen to [RoomUpdate](#roomupdate) ->
checked_locations.
To manually react to others in the same player slot doing checks, listen to [RoomUpdate](#RoomUpdate) -> checked_locations.
### NetworkPlayer
A list of objects. Each object denotes one player. Each object has four fields about the player, in this order: `team`,
`slot`, `alias`, and `name`. `team` and `slot` are ints, `alias` and `name` are strings.
A list of objects. Each object denotes one player. Each object has four fields about the player, in this order: `team`, `slot`, `alias`, and `name`. `team` and `slot` are ints, `alias` and `name` are strs.
Each player belongs to a `team` and has a `slot`. Team numbers start at `0`. Slot numbers are unique per team and start
at `1`. Slot number `0` refers to the Archipelago server; this may appear in instances where the server grants the
player an item.
Each player belongs to a `team` and has a `slot`. Team numbers start at `0`. Slot numbers are unique per team and start at `1`. Slot number `0` refers to the Archipelago server; this may appear in instances where the server grants the player an item.
`alias` represents the player's name in current time. `name` is the original name used when the session was generated.
This is typically distinct in games which require baking names into ROMs or for async games.
`alias` represents the player's name in current time. `name` is the original name used when the session was generated. This is typically distinct in games which require baking names into ROMs or for async games.
```python
from typing import NamedTuple
@@ -503,8 +465,7 @@ In JSON this may look like:
`location` is the location id of the item inside the world. Location ids are in the range of ± 2<sup>53</sup>-1.
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#locationinfo)
Packet then it will be the slot of the player to receive the item.
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item
`flags` are bit flags:
| Flag | Meaning |
@@ -515,8 +476,7 @@ Packet then it will be the slot of the player to receive the item.
| 0b100 | If set, indicates the item is a trap |
### JSONMessagePart
Message nodes sent along with [PrintJSON](#printjson) packet to be reconstructed into a legible message.
The nodes are intended to be read in the order they are listed in the packet.
Message nodes sent along with [PrintJSON](#PrintJSON) packet to be reconstructed into a legible message. The nodes are intended to be read in the order they are listed in the packet.
```python
from typing import TypedDict, Optional
@@ -528,10 +488,7 @@ class JSONMessagePart(TypedDict):
player: Optional[int] # only available if type is either item or location
```
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be
rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all
be-all. Other clients may choose to interpret and display these messages differently.
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently.
Possible values for `type` include:
| Name | Notes |
@@ -547,10 +504,7 @@ Possible values for `type` include:
| color | Regular text that should be colored. Only `type` that will contain `color` data. |
`color` is used to denote a console color to display the message part with and is only send if the `type` is `color`.
This is limited to console colors due to backwards compatibility needs with games such as ALttP.
Although background colors as well as foreground colors are listed, only one may be applied to a
[JSONMessagePart](#jsonmessagepart) at a time.
`color` is used to denote a console color to display the message part with and is only send if the `type` is `color`. This is limited to console colors due to backwards compatibility needs with games such as ALttP. Although background colors as well as foreground colors are listed, only one may be applied to a [JSONMessagePart](#JSONMessagePart) at a time.
Color options:
* bold
@@ -574,11 +528,10 @@ Color options:
`text` is the content of the message part to be displayed.
`player` marks owning player id for location/item,
`flags` contains the [NetworkItem](#networkitem) flags that belong to the item
`flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item
### Client States
An enumeration containing the possible client states that may be used to inform the server in
[StatusUpdate](#statusupdate).
An enumeration containing the possible client states that may be used to inform the server in [StatusUpdate](#StatusUpdate).
```python
import enum
@@ -590,8 +543,7 @@ class ClientStatus(enum.IntEnum):
```
### NetworkVersion
An object representing software versioning. Used in the [Connect](#connect) packet to allow the client to inform the
server of the Archipelago version it supports.
An object representing software versioning. Used in the [Connect](#Connect) packet to allow the client to inform the server of the Archipelago version it supports.
```python
from typing import NamedTuple
class Version(NamedTuple):
@@ -637,14 +589,9 @@ class Permission(enum.IntEnum):
```
### Data Package Contents
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago
server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their
own mappings.
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings.
We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session.
You will know when your cache is outdated if the [RoomInfo](#roominfo) packet or the datapackage itself denote a
different version. A special case is datapackage version 0, where it is expected the package is custom and should not be
cached.
We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different version. A special case is datapackage version 0, where it is expected the package is custom and should not be cached.
Note:
* Any ID is unique to its type across AP: Item 56 only exists once and Location 56 only exists once.
@@ -672,7 +619,7 @@ Tags are represented as a list of strings, the common Client tags follow:
| Name | Notes |
|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
| IgnoreGame | Deprecated. See Tracker and TextOnly. Tells the server to ignore the "game" attribute in the [Connect](#connect) packet. |
| IgnoreGame | Deprecated. See Tracker and TextOnly. Tells the server to ignore the "game" attribute in the [Connect](#Connect) packet. |
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets |
| Tracker | Tells the server that this client will not send locations and is actually a Tracker. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
| TextOnly | Tells the server that this client will not send locations and is intended for chat. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |

View File

@@ -1,108 +0,0 @@
# 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](_static/archipelago-world-directory-example.png)
The minimum requirements for a new archipelago world are the package itself (the world folder containing a file named `__init__.py`),
which must define a `World` class object for the game with a game name, create an equal number of items and locations with rules,
a win condition, and at least one `Region` object.
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++](_static/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++](_static/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++](_static/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++](_static/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++](_static/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++](_static/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]` and
[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md),
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.

View File

@@ -1,225 +0,0 @@
# Adding Games to Archipelago
This guide is going to try and be a broad summary of what is required to add a game integration to Archipelago.
This guide is not an in-depth tutorial on video game modification nor is it a getting started guide to software or
video game development. The intent is to provide information, tips, and tools, to assist a would-be modder in adding a
game integration to Archipelago.
There are two key steps to incorporating a game into Archipelago:
- Game Modification
- Archipelago Server Integration
This document covers game modification. Information on creating the Archipelago server integration may be found in the
[Adding Archipelago Integration](./AddingArchipelagoIntegration.md).
## 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, this is described further down.
As an example, modifications to a game typically include:
- 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.
### 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
##### Proprietary Game Engine
![Creepy Castle Root Directory in Window's Explorer](_static/creepy-castle-directory.png)
This is the game _Creepy Castle_. Its 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.
##### Unity Game Engine
![Heavy Bullets Root Directory in Window's Explorer](_static/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 text files in their directories
in some form, usually with a name like `README.txt`. They may contain information about a game, such as a EULA, terms
of service, licensing information, credits, or other general info about the game. You typically 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](_static/heavy-bullets-data-directory.png)
The contents of the `HEAVY_BULLETS_Data` directory follow the pattern typically used by the Unity game 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.
Also keep your eyes out for an executable with a name like UnityCrashHandler, thats another dead giveaway.
##### XNA/FNA
![Stardew Valley Root Directory in Window's Explorer](_static/stardew-valley-directory.png)
This is the game contents of _Stardew Valley_.
A lot more to look at here, but there are some key takeaways. Notice the .dll files which include “CSharp” in their
name. Also notice the `Content`. These signs point to a game based on the .NET framework and many games following this
style will use the XNA game framework as the base to build their game from.
##### Gato Roboto
![Gato Roboto Root Directory in Window's Explorer](_static/gato-roboto-directory.png)
Our last example is the game _Gato Roboto_. Notice the file titled `data.win`. This immediately tips us off that this
game was made in GameMaker.
### 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
from the author to do so, nor to redistribute any materials obtained through similar methods, as this is illegal and
unethical.
### Modifying Release Versions of Games
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. This is usually assessed on a case-by-case
basis. Games with large modding communities typically grow around the tooling a developer provides or they grow around
the fact that the game is easy to modify in the first place.
As a general rule, any modding tool that lets you write actual code is something worth using.
### Creating the Mod
#### 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.
##### ILSpy
You can download ILSpy and see more info about it on the [ILSpy GitHub repository homepage](https://github.com/icsharpcode/ILSpy).
The first tool in your toolbox is ILSpy. ILSpy is a .NET decompiler. The purpose of this program is to take a compiled
.NET assembly (.DLL or .EXE file) and turn it back into human-readable source code. A file is a .NET assembly when it
was created through the compilation of any programming language targeting the .NET runtime. Usually, the programming
language in question is C# (C Sharp).
Unity games are a combination of native code (compiled in a "native language" such as C++) and .NET code. Most game
developers will write the bulk of their code as C# and this will be compiled by Unity into .NET assemblies. Those files
may then be decompiled with ILSpy to allow you to see the original source code of the game.
For Unity games, the file youll typically want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as
pictured below:
![Heavy Bullets Managed Directory in Window's Explorer](_static/heavy-bullets-managed-directory.png)
For other .NET based games, which are not made in Unity, the file you want is usually just the executable itself.
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
You can download and find more info on UndertaleModTool on the [UndertaleModTool GitHub repository homepage](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 GameMaker Studio
1 and 2. It allows you to modify code in GameMaker Language (GML).
Use the tool to open the `data.win` file to see game data and code.
Like ILSpy, 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.
#### 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](../NetworkProtocol.md) for how to communicate with Archipelago's servers.
### Modifying Console Games
#### 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.
There is some traction on this changing as studios are finding ways to include game modifications in console games.
#### My Game isnt that old, its for the Wii/PS2/360/etc
This is very complex, but doable.
It is typically necessary to use Assembly (ASM) to modify these games.
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. 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. You're welcome to try and break ground on something like this, but understand that community
support will range from "extremely limited" to "nonexistent".
### How to Distribute Game Modifications
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
The right way to distribute modified versions of a game's binaries is to distribute binary patches.
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 without including the original file's content, 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.
#### Patches
The following patch formats are commonly seen in the game modding scene.
##### IPS
IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
fine.
##### UPS, BPS, VCDIFF (xdelta), bsdiff
Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
compression, so this format is used by APBP.
Only a bsdiff module is integrated into AP. If the final patch requires or is based on any other patch, convert them to
bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
##### APBP Archipelago Binary Patch
Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
bsdiff between the original and the randomized ROM.
To make using APBP easy, they can be generated by inheriting from `Patch.APDeltaPatch`.
#### 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.
They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
generated per seed.
If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
integration into the Webhost by inheriting from `Patch.APContainer`.

View File

@@ -1,101 +0,0 @@
# Archipelago Architecture
Archipelago is split into several components. All components must operate in tandem to facilitate randomization
and gameplay.
The components are:
* [Archipelago Generator](#archipelago-generator)
* [Archipelago Server](#archipelago-server)
* [Archipelago Game Client](#archipelago-game-client)
Some games require additional components in order to facilitate gameplay or communication with Archipelago.
The additional components vary from game to game but are typically:
* [Retro Console Emulator](#retro-console-emulator)
* [Emulator Communication Bridge (SNI)](#emulator-communication-bridge)
## Archipelago Generator
The Archipelago Generator is the part of Archipelago which takes YAML configuration files as input and produces a ZIP
file containing the data necessary for the Archipelago Server to service a session. The generator software is standalone
from the server or game clients and is run outside the server context. The server may then be pointed to the resulting
file to serve that session.
For more information on using the Archipelago Generator as a user, please visit the user facing MultiWorld Setup Guide
section on [Rolling a YAML Locally](https://archipelago.gg/tutorial/Archipelago/setup/en#rolling-a-yaml-locally).
The Generator functions by using the classes defined in the `/worlds` folder to understand each game's items, location,
YAML options, and logic. The "World" classes define these properties in code and are loaded by the generator to allow it
to validate YAML options and create a multiworld with cohesive and solvable logic despite the possibility of disparate
games being played.
## Archipelago Server
The Archipelago Server facilitates gameplay for a multiworld session. A session may have any number of players.
As Archipelago is client-server software the server is still required for sessions even if only a single player is
present. The server takes a ZIP file or an ARCHIPELAGO file as input and serves the session using the information from
the input to properly serve the game clients over network.
## Archipelago Game Client
Archipelago game clients are currently implemented in two main ways. The first are in-process clients, which operate as
a mod loaded within the game process. The game process will then facilitate the WebSocket communication with the
Archipelago Server. Typically, more "modern" games will use this approach as they are typically easier to mod or are
easier to inject with code at runtime.
Some examples of Archipelago games implementing the in-process model are:
* [Risk of Rain 2](https://github.com/Ijwu/Archipelago.RiskOfRain2)
* [Subnautica](https://github.com/Berserker66/ArchipelagoSubnauticaModSrc)
* [Hollow Knight](https://github.com/Ijwu/Archipelago.HollowKnight)
The in-process model can be visualized using the following diagram:
```{mermaid}
flowchart LR
APS[Archipelago Server]
APGC[Archipelago Game Client]
APS <-- WebSockets --> APGC
```
The second model of game client are those which operate out-of-process. The out-of-process clients are shipped with the
Archipelago installation and live within the Archipelago codebase. They are implemented in Python using [CommonClient.py](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py)
as a base. This client model is typically used for games in which runtime modification is difficult to impossible and for
games which require additional components such as the emulator communication bridge. This model is also used for clients
which communicate with the game from outside the game process to understand game state; the client then communicates
updates to the Archipelago server based on the game state.
Some examples of Archipelago games implementing the out-of-process model are:
* [Starcraft 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/Starcraft2Client.py)
* [Factorio](https://github.com/ArchipelagoMW/Archipelago/blob/main/FactorioClient.py)
* [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Archipelago/blob/main/OoTClient.py)
The out-of-process model can be visualized using the following diagram:
```{mermaid}
flowchart LR
APS[Archipelago Server]
OOPGC[Out-of-Process Game Client]
GP[Game Process]
APS <-- WebSockets --> OOPGC <--> GP
```
Games which use the [SNI](https://github.com/alttpo/sni) emulator communication bridge can be connected to Archipelago using the [SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py).
Games communicating using SNI may be visualized using the following diagram:
```{mermaid}
flowchart LR
APS[Archipelago Server]
SNIC[SNIClient]
SNI[SNI]
GP[Game Process]
APS <-- WebSockets --> SNIC <-- WebSockets --> SNI <--> GP
```
## Retro Console Emulator
Some game implementations require the use of an emulator in order to run the game and to communicate with SNI.
These games are typically "retro" games which were released on 8-bit or 16-bit consoles, although newer consoles may be
included for some game implementations.
All emulators currently used in Archipelago game implementations which require them are lua enabled and use a lua script
to communicate with SNI.
## Emulator Communication Bridge
All implementations of game clients for which the game is run in an emulator presently use [SuperNintendoInterface or SNI](https://github.com/alttpo/sni)
to communicate between the emulator and the SNIClient. The emulator uses lua to communicate to SNI which communicates with
the SNIClient which communicates with the Archipelago server.

View File

@@ -1,19 +0,0 @@
World Class
===========
```{eval-rst}
.. currentmodule:: worlds.AutoWorld
.. autoclass:: World
:members: options, game, topology_present, all_item_and_group_names,
item_name_to_id, location_name_to_id, item_name_groups, data_version,
required_client_version, required_server_version, hint_blacklist,
remote_items, remote_start_inventory, forced_auto_forfeit, hidden,
world, player, item_id_to_name, location_id_to_name, item_names,
location_names, web, assert_generate, generate_early, create_regions,
create_items, set_rules, generate_basic, pre_fill, fill_hook, post_fill,
generate_output, fill_slot_data, modify_multidata, write_spoiler_header,
write_spoiler, write_spoiler_end, create_item, get_filler_item_name,
collect_item, get_pre_fill_items
:undoc-members:
:special-members: __init__
```

View File

@@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -1,56 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('../..'))
# -- Project information -----------------------------------------------------
project = 'Archipelago'
copyright = '2022, Archipelago Team and Contributors'
author = 'Archipelago Team and Contributors'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"myst_parser",
"sphinxcontrib.mermaid",
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
myst_heading_anchors = 4
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

View File

@@ -1,70 +0,0 @@
% Archipelago documentation master file, created by
% sphinx-quickstart on Wed Jul 6 20:09:51 2022.
% You can adapt this file completely to your liking, but it should at least
% contain the root `toctree` directive.
Welcome to Archipelago's Technical Documentation!
=================================================
## What is Archipelago?
Archipelago provides a generic framework for developing multiworld capability for game randomizers.
In all cases, presently, Archipelago is also the randomizer itself.
Archipelago is end-user facing software intended to facilitate randomizer and multiworld play for a variety of
supported games.
Archipelago presently supports the following games:
* The Legend of Zelda: A Link to the Past
* Factorio
* Minecraft
* Subnautica
* Slay the Spire
* Risk of Rain 2
* The Legend of Zelda: Ocarina of Time
* Timespinner
* Super Metroid
* Secret of Evermore
* Final Fantasy
* Rogue Legacy
* VVVVVV
* Raft
* Super Mario 64
* Meritous
* Super Metroid/Link to the Past combo randomizer (SMZ3)
* ChecksFinder
* ArchipIDLE
* Hollow Knight
* The Witness
* Sonic Adventure 2: Battle
* Starcraft 2: Wings of Liberty
* Donkey Kong Country 3
* Dark Souls 3
For more information on the technical architecture of Archipelago,
please refer to the [Archipelago Technical Architecture](Architecture.md) document.
## Contributing to Archipelago
Contributions to the Archipelago code are welcome and dearly appreciated. Contributions may occur as changes to website
content, changes to Archipelago core code, additions of game integrations, or alterations to website functionality.
Please visit our [contributing guidelines on our GitHub README](https://github.com/ArchipelagoMW/Archipelago#contributing)
for some more information on what may be expected.
For information on contributing a game integration, check out our [document on adding games to Archipelago](./AddingGames.md).
## Table of Contents
```{toctree}
---
maxdepth: 2
caption: "Documentation contents:"
---
AddingGames
WorldAPI
NetworkProtocol
NetworkDiagram
```
## Indices and tables
* {ref}`genindex`
* {ref}`modindex`
* {ref}`search`

View File

@@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@@ -1,4 +0,0 @@
sphinx==5.0.2
sphinx_rtd_theme==1.0.0
readthedocs-sphinx-search==0.1.2
myst-parser==0.18

View File

@@ -6,20 +6,26 @@ required to send and receive items between the game and server.
Client implementation is out of scope of this document. Please refer to an
existing game that provides a similar API to yours.
Refer to the following documents as well:
- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md)
- [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md)
Archipelago will be abbreviated as "AP" from now on.
## Language
AP worlds are written in python3.
Clients that connect to the server to sync items can be in any language that
allows using WebSockets.
## Coding style
AP follows all the PEPs. When in doubt use an IDE with coding style
linter, for example PyCharm Community Edition.
## Docstrings
Docstrings are strings attached to an object in Python that describe what the
@@ -34,6 +40,7 @@ class MyGameWorld(World):
website."""
```
## Definitions
This section will cover various classes and objects you can use for your world.
@@ -56,7 +63,7 @@ for your world specifically on the webhost.
`theme` to be used for your game specific AP pages. Available themes:
| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone |
|---|---|---|---|---|---|---|---|
| <img src="_static/theme_dirt.JPG" width="200"> | <img src="_static/theme_grass.JPG" width="200"> | <img src="_static/theme_grassFlowers.JPG" width="200"> | <img src="_static/theme_ice.JPG" width="200"> | <img src="_static/theme_jungle.JPG" width="200"> | <img src="_static/theme_ocean.JPG" width="200"> | <img src="_static/theme_partyTime.JPG" width="200"> | <img src="_static/theme_stone.JPG" width="200"> |
| <img src="img/theme_dirt.JPG" width="100"> | <img src="img/theme_grass.JPG" width="100"> | <img src="img/theme_grassFlowers.JPG" width="100"> | <img src="img/theme_ice.JPG" width="100"> | <img src="img/theme_jungle.JPG" width="100"> | <img src="img/theme_ocean.JPG" width="100"> | <img src="img/theme_partyTime.JPG" width="100"> | <img src="img/theme_stone.JPG" width="100"> |
`bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be placed by the site to help direct users to report bugs.
@@ -96,8 +103,9 @@ or boss drops for RPG-like games but could also be progress in a research tree.
Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed
in a Region and has access rules.
The name needs to be unique in each game, the ID needs to be unique across all
games and is best in the same range as the item IDs.
The name needs to be unique in each game and must not be numeric (has to
contain least 1 letter or symbol). The ID needs to be unique across all games
and is best in the same range as the item IDs.
World-specific IDs are 1 to 2<sup>53</sup>-1, IDs ≤ 0 are global and reserved.
Special locations with ID `None` can hold events.
@@ -114,6 +122,9 @@ their world. Progression items will be assigned to locations with higher
priority and moved around to meet defined rules and accomplish progression
balancing.
The name needs to be unique in each game, meaning a duplicate item has the
same ID. Name must not be numeric (has to contain at least 1 letter or symbol).
Special items with ID `None` can mark events (read below).
Other classifications include
@@ -181,15 +192,17 @@ the `/worlds` directory. The starting point for the package is `__init.py__`.
Conventionally, your world class is placed in that file.
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
which can be imported as `..AutoWorld.World` from your package.
which can be imported as `worlds.AutoWorld.World` from your package.
AP will pick up your world automatically due to the `AutoWorld` implementation.
### Requirements
If your world needs specific python packages, they can be listed in
`world/[world_name]/requirements.txt`.
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format)
`world/[world_name]/requirements.txt`. ModuleUpdate.py will automatically
pick up and install them.
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format).
### Relative Imports
@@ -202,6 +215,10 @@ e.g. `from .Options import mygame_options` from your `__init__.py` will load
When imported names pile up it may be easier to use `from . import Options`
and access the variable as `Options.mygame_options`.
Imports from directories outside your world should use absolute imports.
Correct use of relative / absolute imports is required for zipped worlds to
function, see [apworld specification.md](apworld%20specification.md).
### Your Item Type
Each world uses its own subclass of `BaseClasses.Item`. The constuctor can be
@@ -267,14 +284,12 @@ Define a property `option_<name> = <number>` per selectable value and
`default = <number>` to set the default selection. Aliases can be set by
defining a property `alias_<name> = <same number>`.
One special case where aliases are required is when option name is `yes`, `no`,
`on` or `off` because they parse to `True` or `False`:
```python
option_off = 0
option_on = 1
option_some = 2
alias_false = 0
alias_true = 1
alias_disabled = 0
alias_enabled = 1
default = 0
```
@@ -316,7 +331,7 @@ mygame_options: typing.Dict[str, type(Option)] = {
```python
# __init__.py
from ..AutoWorld import World
from worlds.AutoWorld import World
from .Options import mygame_options # import the options dict
class MyGameWorld(World):
@@ -345,7 +360,7 @@ more natural. These games typically have been edited to 'bake in' the items.
from .Options import mygame_options # the options we defined earlier
from .Items import mygame_items # data used below to add items to the World
from .Locations import mygame_locations # same as above
from ..AutoWorld import World
from worlds.AutoWorld import World
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
from Utils import get_options, output_path
@@ -546,7 +561,7 @@ def generate_basic(self) -> None:
### Setting Rules
```python
from ..generic.Rules import add_rule, set_rule, forbid_item
from worlds.generic.Rules import add_rule, set_rule, forbid_item
from Items import get_item_type
def set_rules(self) -> None:
@@ -596,7 +611,7 @@ implement more complex logic in logic mixins, even if there is no need to add
properties to the `BaseClasses.CollectionState` state object.
When importing a file that defines a class that inherits from
`..AutoWorld.LogicMixin` the state object's class is automatically extended by
`worlds.AutoWorld.LogicMixin` the state object's class is automatically extended by
the mixin's members. These members should be prefixed with underscore following
the name of the implementing world. This is due to sharing a namespace with all
other logic mixins.
@@ -615,7 +630,7 @@ Please do this with caution and only when neccessary.
```python
# Logic.py
from ..AutoWorld import LogicMixin
from worlds.AutoWorld import LogicMixin
class MyGameLogic(LogicMixin):
def _mygame_has_key(self, world: MultiWorld, player: int):
@@ -626,7 +641,7 @@ class MyGameLogic(LogicMixin):
```python
# __init__.py
from ..generic.Rules import set_rule
from worlds.generic.Rules import set_rule
import .Logic # apply the mixin by importing its file
class MyGameWorld(World):

View File

@@ -196,7 +196,7 @@ begin
begin
// Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
Result := (CompareStr(strVersion, 'v14.32.31332') < 0);
end
else
begin

View File

@@ -371,13 +371,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
distribute_items_restrictive(multi_world)
self.assertEqual(locations[0].item, basic_items[0])
self.assertEqual(locations[0].item, basic_items[1])
self.assertFalse(locations[0].event)
self.assertEqual(locations[1].item, prog_items[0])
self.assertTrue(locations[1].event)
self.assertEqual(locations[2].item, prog_items[1])
self.assertTrue(locations[2].event)
self.assertEqual(locations[3].item, basic_items[1])
self.assertEqual(locations[3].item, basic_items[0])
self.assertFalse(locations[3].event)
def test_excluded_distribute(self):
@@ -500,8 +500,8 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
removed_item: list[Item] = []
removed_location: list[Location] = []
def fill_hook(progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations):
removed_item.append(restitempool.pop(0))
def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations):
removed_item.append(filleritempool.pop(0))
removed_location.append(fill_locations.pop(0))
multi_world.worlds[player1.id].fill_hook = fill_hook

20
test/general/TestNames.py Normal file
View File

@@ -0,0 +1,20 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister
class TestNames(unittest.TestCase):
def testItemNamesFormat(self):
"""Item names must not be all numeric in order to differentiate between ID and name in !hint"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
for item_name in world_type.item_name_to_id:
self.assertFalse(item_name.isnumeric(),
f"Item name \"{item_name}\" is invalid. It must not be numeric.")
def testLocationNameFormat(self):
"""Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
for location_name in world_type.location_name_to_id:
self.assertFalse(location_name.isnumeric(),
f"Location name \"{location_name}\" is invalid. It must not be numeric.")

View File

@@ -14,9 +14,20 @@ class TestFileGeneration(unittest.TestCase):
def testOptions(self):
WebHost.create_options_files()
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "configs")))
target = os.path.join(self.correct_path, "static", "generated", "configs")
self.assertTrue(os.path.exists(target))
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))
# folder seems fine, so now we try to generate Options based on the default file
from WebHostLib.check import roll_options
file: os.DirEntry
for file in os.scandir(target):
if file.is_file() and file.name.endswith(".yaml"):
with self.subTest(file=file.name):
with open(file) as f:
for value in roll_options({file.name: f.read()})[0].values():
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
def testTutorial(self):
WebHost.create_ordered_tutorials_file()
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))

View File

@@ -120,87 +120,67 @@ class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
A Game should have its own subclass of World in which it defines the required data structures."""
option_definitions: Dict[str, Option[Any]] = {}
""" link your Options mapping """
game: str
""" name of the game the world is for """
topology_present: bool = False
""" indicate if world type has any meaningful layout/pathing """
option_definitions: Dict[str, Option[Any]] = {} # link your Options mapping
game: str # name the game
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
# gets automatically populated with all item and item group names
all_item_and_group_names: FrozenSet[str] = frozenset()
""" gets automatically populated with all item and item group names """
# map names to their IDs
item_name_to_id: Dict[str, int] = {}
""" map item names to their IDs """
location_name_to_id: Dict[str, int] = {}
""" map location names to their IDs """
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
item_name_groups: Dict[str, Set[str]] = {}
""" maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"} """
# increment this every time something in your world's names/id mappings changes.
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
# retrieved by clients on every connection.
data_version: int = 1
"""increment this every time something in your world's names/id mappings changes.
While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
retrieved by clients on every connection.
"""
# override this if changes to a world break forward-compatibility of the client
# The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
# future. Protocol level compatibility check moved to MultiServer.min_client_version.
required_client_version: Tuple[int, int, int] = (0, 1, 6)
""" override this if changes to a world break forward-compatibility of the client
The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
future. Protocol level compatibility check moved to MultiServer.min_client_version.
"""
# update this if the resulting multidata breaks forward-compatibility of the server
required_server_version: Tuple[int, int, int] = (0, 2, 4)
""" update this if the resulting multidata breaks forward-compatibility of the server """
hint_blacklist: FrozenSet[str] = frozenset()
""" any names that should not be hintable """
hint_blacklist: FrozenSet[str] = frozenset() # any names that should not be hintable
# NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set.
# These values will be removed.
# if a world is set to remote_items, then it just needs to send location checks to the server and the server
# sends back the items
# if a world is set to remote_items = False, then the server never sends an item where receiver == finder,
# the client finds its own items in its own world.
remote_items: bool = True
""" NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set.
These values will be removed.
if a world is set to remote_items, then it just needs to send location checks to the server and the server
sends back the items
if a world is set to remote_items = False, then the server never sends an item where receiver == finder,
the client finds its own items in its own world.
"""
# 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
""" 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.
"""
# 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
""" 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.
"""
# Hide World Type from various views. Does not remove functionality.
hidden: bool = False
""" Hide World Type from various views. Does not remove functionality. """
# see WebWorld for options
web: WebWorld = WebWorld()
# autoset on creation:
world: "MultiWorld"
""" autoset on creation """
player: int
""" autoset on creation """
# automatically generated
item_id_to_name: Dict[int, str]
""" automatically generated inverse of item_name_to_id """
location_id_to_name: Dict[int, str]
""" automatically generated inverse of location_name_to_id """
item_names: Set[str]
""" set of all potential item names """
location_names: Set[str]
""" set of all potential location names """
item_names: Set[str] # set of all potential item names
location_names: Set[str] # set of all potential location names
zip_path: Optional[pathlib.Path] = None # If loaded from a .apworld, this is the Path to it.
__file__: str # path it was loaded from
@@ -241,10 +221,8 @@ class World(metaclass=AutoWorldRegister):
@classmethod
def fill_hook(cls,
progitempool: List["Item"],
nonexcludeditempool: List["Item"],
localrestitempool: Dict[int, List["Item"]],
nonlocalrestitempool: Dict[int, List["Item"]],
restitempool: List["Item"],
usefulitempool: List["Item"],
filleritempool: List["Item"],
fill_locations: List["Location"]) -> None:
"""Special method that gets called as part of distribute_items_restrictive (main fill).
This gets called once per present world type."""
@@ -262,6 +240,11 @@ class World(metaclass=AutoWorldRegister):
"""Fill in the slot_data field in the Connected network package."""
return {}
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
"""Fill in additional entrance information text into locations, which is displayed when hinted.
structure is {player_id: {location_id: text}} You will need to insert your own player_id."""
pass
def modify_multidata(self, multidata: Dict[str, Any]) -> None: # TODO: TypedDict for multidata?
"""For deeper modification of server multidata."""
pass
@@ -304,8 +287,8 @@ class World(metaclass=AutoWorldRegister):
return item.name
return None
# called to create all_state, return Items that are created during pre_fill
def get_pre_fill_items(self) -> List["Item"]:
""" called to create all_state, return Items that are created during pre_fill """
return []
# following methods should not need to be overridden.

View File

@@ -5,15 +5,14 @@ import typing
folder = os.path.dirname(__file__)
__all__ = [
__all__ = {
"lookup_any_item_id_to_name",
"lookup_any_location_id_to_name",
"network_data_package",
"AutoWorldRegister",
"world_sources",
"folder",
"World"
]
}
if typing.TYPE_CHECKING:
from .AutoWorld import World
@@ -28,7 +27,8 @@ class WorldSource(typing.NamedTuple):
world_sources: typing.List[WorldSource] = []
file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
for file in os.scandir(folder):
if not file.name.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders
# prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
if not file.name.startswith(("_", ".")):
if file.is_dir():
world_sources.append(WorldSource(file.name))
elif file.is_file() and file.name.endswith(".apworld"):

View File

@@ -1,8 +1,9 @@
import logging
from typing import Optional
from typing import Optional, Union, List, Tuple, Callable, Dict
from BaseClasses import Boss
from Fill import FillError
from .Options import Bosses
def BossFactory(boss: str, player: int) -> Optional[Boss]:
@@ -12,7 +13,7 @@ def BossFactory(boss: str, player: int) -> Optional[Boss]:
raise Exception('Unknown Boss: %s', boss)
def ArmosKnightsDefeatRule(state, player: int):
def ArmosKnightsDefeatRule(state, player: int) -> bool:
# Magic amounts are probably a bit overkill
return (
state.has_melee_weapon(player) or
@@ -25,7 +26,7 @@ def ArmosKnightsDefeatRule(state, player: int):
state.has('Red Boomerang', player))
def LanmolasDefeatRule(state, player: int):
def LanmolasDefeatRule(state, player: int) -> bool:
return (
state.has_melee_weapon(player) or
state.has('Fire Rod', player) or
@@ -35,16 +36,16 @@ def LanmolasDefeatRule(state, player: int):
state.can_shoot_arrows(player))
def MoldormDefeatRule(state, player: int):
def MoldormDefeatRule(state, player: int) -> bool:
return state.has_melee_weapon(player)
def HelmasaurKingDefeatRule(state, player: int):
def HelmasaurKingDefeatRule(state, player: int) -> bool:
# TODO: technically possible with the hammer
return state.has_sword(player) or state.can_shoot_arrows(player)
def ArrghusDefeatRule(state, player: int):
def ArrghusDefeatRule(state, player: int) -> bool:
if not state.has('Hookshot', player):
return False
# TODO: ideally we would have a check for bow and silvers, which combined with the
@@ -58,7 +59,7 @@ def ArrghusDefeatRule(state, player: int):
(state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16))))
def MothulaDefeatRule(state, player: int):
def MothulaDefeatRule(state, player: int) -> bool:
return (
state.has_melee_weapon(player) or
(state.has('Fire Rod', player) and state.can_extend_magic(player, 10)) or
@@ -70,11 +71,11 @@ def MothulaDefeatRule(state, player: int):
)
def BlindDefeatRule(state, player: int):
def BlindDefeatRule(state, player: int) -> bool:
return state.has_melee_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player)
def KholdstareDefeatRule(state, player: int):
def KholdstareDefeatRule(state, player: int) -> bool:
return (
(
state.has('Fire Rod', player) or
@@ -96,11 +97,11 @@ def KholdstareDefeatRule(state, player: int):
)
def VitreousDefeatRule(state, player: int):
def VitreousDefeatRule(state, player: int) -> bool:
return state.can_shoot_arrows(player) or state.has_melee_weapon(player)
def TrinexxDefeatRule(state, player: int):
def TrinexxDefeatRule(state, player: int) -> bool:
if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)):
return False
return state.has('Hammer', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) or \
@@ -108,11 +109,11 @@ def TrinexxDefeatRule(state, player: int):
(state.has_sword(player) and state.can_extend_magic(player, 32))
def AgahnimDefeatRule(state, player: int):
def AgahnimDefeatRule(state, player: int) -> bool:
return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player)
def GanonDefeatRule(state, player: int):
def GanonDefeatRule(state, player: int) -> bool:
if state.world.swordless[player]:
return state.has('Hammer', player) and \
state.has_fire_source(player) and \
@@ -132,7 +133,7 @@ def GanonDefeatRule(state, player: int):
return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player)
boss_table = {
boss_table: Dict[str, Tuple[str, Optional[Callable]]] = {
'Armos Knights': ('Armos', ArmosKnightsDefeatRule),
'Lanmolas': ('Lanmola', LanmolasDefeatRule),
'Moldorm': ('Moldorm', MoldormDefeatRule),
@@ -147,7 +148,7 @@ boss_table = {
'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
}
boss_location_table = [
boss_location_table: List[Tuple[str, str]] = [
('Ganons Tower', 'top'),
('Tower of Hera', None),
('Skull Woods', None),
@@ -164,6 +165,34 @@ boss_location_table = [
]
def place_plando_bosses(bosses: List[str], world, player: int) -> Tuple[List[str], List[Tuple[str, str]]]:
# Most to least restrictive order
boss_locations = boss_location_table.copy()
world.random.shuffle(boss_locations)
boss_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
already_placed_bosses: List[str] = []
for boss in bosses:
if "-" in boss: # handle plando locations
loc, boss = boss.split("-")
boss = boss.title()
level: str = None
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
# split off level
loc = loc.split(" ")
level = loc[-1]
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
place_boss(world, player, boss, loc, level)
already_placed_bosses.append(boss)
boss_locations.remove((loc, level))
else: # boss chosen with no specified locations
boss = boss.title()
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations)
return already_placed_bosses, boss_locations
def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> bool:
# blacklist approach
if boss in {"Agahnim", "Agahnim2", "Ganon"}:
@@ -187,62 +216,50 @@ def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) ->
return True
restrictive_boss_locations = {}
restrictive_boss_locations: Dict[Tuple[str, str], bool] = {}
for location in boss_location_table:
restrictive_boss_locations[location] = not all(can_place_boss(boss, *location)
for boss in boss_table if not boss.startswith("Agahnim"))
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]):
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]) -> None:
if location == 'Ganons Tower' and world.mode[player] == 'inverted':
location = 'Inverted Ganons Tower'
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player)
def format_boss_location(location, level):
def format_boss_location(location: str, level: str) -> str:
return location + (' (' + level + ')' if level else '')
def place_bosses(world, player: int):
if world.boss_shuffle[player] == 'none':
def place_bosses(world, player: int) -> None:
# will either be an int or a lower case string with ';' between options
boss_shuffle: Union[str, int] = world.boss_shuffle[player].value
already_placed_bosses: List[str] = []
remaining_locations: List[Tuple[str, str]] = []
# handle plando
if isinstance(boss_shuffle, str):
# figure out our remaining mode, convert it to an int and remove it from plando_args
options = boss_shuffle.split(";")
boss_shuffle = Bosses.options[options.pop()]
# place our plando bosses
already_placed_bosses, remaining_locations = place_plando_bosses(options, world, player)
if boss_shuffle == Bosses.option_none: # vanilla boss locations
return
# Most to least restrictive order
boss_locations = boss_location_table.copy()
world.random.shuffle(boss_locations)
boss_locations.sort(key= lambda location: -int(restrictive_boss_locations[location]))
if not remaining_locations and not already_placed_bosses:
remaining_locations = boss_location_table.copy()
world.random.shuffle(remaining_locations)
remaining_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
shuffle_mode = world.boss_shuffle[player]
already_placed_bosses = []
if ";" in shuffle_mode:
bosses = shuffle_mode.split(";")
shuffle_mode = bosses.pop()
for boss in bosses:
if "-" in boss:
loc, boss = boss.split("-")
boss = boss.title()
level = None
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
# split off level
loc = loc.split(" ")
level = loc[-1]
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
if can_place_boss(boss, loc, level) and (loc, level) in boss_locations:
place_boss(world, player, boss, loc, level)
already_placed_bosses.append(boss)
boss_locations.remove((loc, level))
else:
raise Exception(f"Cannot place {boss} at {format_boss_location(loc, level)} for player {player}.")
else:
boss = boss.title()
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations)
if shuffle_mode == "none":
return # vanilla bosses come pre-placed
if shuffle_mode in ["basic", "full"]:
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
if boss_shuffle == Bosses.option_basic or boss_shuffle == Bosses.option_full:
if boss_shuffle == Bosses.option_basic: # vanilla bosses shuffled
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random
bosses = placeable_bosses + world.random.sample(placeable_bosses, 3)
@@ -258,7 +275,7 @@ def place_bosses(world, player: int):
logging.debug('Bosses chosen %s', bosses)
world.random.shuffle(bosses)
for loc, level in boss_locations:
for loc, level in remaining_locations:
for _ in range(len(bosses)):
boss = bosses.pop()
if can_place_boss(boss, loc, level):
@@ -272,8 +289,8 @@ def place_bosses(world, player: int):
place_boss(world, player, boss, loc, level)
elif shuffle_mode == "chaos": # all bosses chosen at random
for loc, level in boss_locations:
elif boss_shuffle == Bosses.option_chaos: # all bosses chosen at random
for loc, level in remaining_locations:
try:
boss = world.random.choice(
[b for b in placeable_bosses if can_place_boss(b, loc, level)])
@@ -282,9 +299,9 @@ def place_bosses(world, player: int):
else:
place_boss(world, player, boss, loc, level)
elif shuffle_mode == "singularity":
elif boss_shuffle == Bosses.option_singularity:
primary_boss = world.random.choice(placeable_bosses)
remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, boss_locations)
remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, remaining_locations)
if remaining_boss_locations:
# pick a boss to go into the remaining locations
remaining_boss = world.random.choice([boss for boss in placeable_bosses if all(
@@ -293,12 +310,12 @@ def place_bosses(world, player: int):
if remaining_boss_locations:
raise Exception("Unfilled boss locations!")
else:
raise FillError(f"Could not find boss shuffle mode {shuffle_mode}")
raise FillError(f"Could not find boss shuffle mode {boss_shuffle}")
def place_where_possible(world, player: int, boss: str, boss_locations):
remainder = []
placed_bosses = []
def place_where_possible(world, player: int, boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]:
remainder: List[Tuple[str, str]] = []
placed_bosses: List[str] = []
for loc, level in boss_locations:
# place that boss where it can go
if can_place_boss(boss, loc, level):

View File

@@ -480,7 +480,7 @@ def set_up_take_anys(world, player):
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
world.shops.append(old_man_take_any.shop)
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
swords = [item for item in world.itempool if item.player == player and item.type == 'Sword']
if swords:
sword = world.random.choice(swords)
world.itempool.remove(sword)

View File

@@ -1,7 +1,7 @@
import typing
from BaseClasses import MultiWorld
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice
class Logic(Choice):
@@ -39,8 +39,6 @@ class OpenPyramid(Choice):
option_auto = 3
default = option_goal
alias_true = option_open
alias_false = option_closed
alias_yes = option_open
alias_no = option_closed
@@ -140,13 +138,143 @@ class WorldState(Choice):
option_inverted = 2
class Bosses(Choice):
option_vanilla = 0
option_simple = 1
class Bosses(TextChoice):
"""Shuffles bosses around to different locations.
Basic will shuffle all bosses except Ganon and Agahnim anywhere they can be placed.
Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur.
Chaos allows any boss to appear any number of times.
Singularity places a single boss in as many places as possible, and a second boss in any remaining locations.
Supports plando placement. Formatting here: https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/plando/en"""
display_name = "Boss Shuffle"
option_none = 0
option_basic = 1
option_full = 2
option_chaos = 3
option_singularity = 4
bosses: set = {
"Armos Knights",
"Lanmolas",
"Moldorm",
"Helmasaur King",
"Arrghus",
"Mothula",
"Blind",
"Kholdstare",
"Vitreous",
"Trinexx",
}
locations: set = {
"Ganons Tower Top",
"Tower of Hera",
"Skull Woods",
"Ganons Tower Middle",
"Eastern Palace",
"Desert Palace",
"Palace of Darkness",
"Swamp Palace",
"Thieves Town",
"Ice Palace",
"Misery Mire",
"Turtle Rock",
"Ganons Tower Bottom"
}
def __init__(self, value: typing.Union[str, int]):
assert isinstance(value, str) or isinstance(value, int), \
f"{value} is not a valid option for {self.__class__.__name__}"
self.value = value
@classmethod
def from_text(cls, text: str):
import random
# set all of our text to lower case for name checking
text = text.lower()
cls.bosses = {boss_name.lower() for boss_name in cls.bosses}
cls.locations = {boss_location.lower() for boss_location in cls.locations}
if text == "random":
return cls(random.choice(list(cls.options.values())))
for option_name, value in cls.options.items():
if option_name == text:
return cls(value)
options = text.split(";")
# since plando exists in the option verify the plando values given are valid
cls.validate_plando_bosses(options)
# find out what type of boss shuffle we should use for placing bosses after plando
# and add as a string to look nice in the spoiler
if "random" in options:
shuffle = random.choice(list(cls.options))
options.remove("random")
options = ";".join(options) + ";" + shuffle
boss_class = cls(options)
else:
for option in options:
if option in cls.options:
boss_class = cls(";".join(options))
break
else:
if len(options) == 1:
if cls.valid_boss_name(options[0]):
options = options[0] + ";singularity"
boss_class = cls(options)
else:
options = options[0] + ";none"
boss_class = cls(options)
else:
options = ";".join(options) + ";none"
boss_class = cls(options)
return boss_class
@classmethod
def validate_plando_bosses(cls, options: typing.List[str]) -> None:
from .Bosses import can_place_boss, format_boss_location
for option in options:
if option == "random" or option in cls.options:
if option != options[-1]:
raise ValueError(f"{option} option must be at the end of the boss_shuffle options!")
continue
if "-" in option:
location, boss = option.split("-")
level = ''
if not cls.valid_boss_name(boss):
raise ValueError(f"{boss} is not a valid boss name for location {location}.")
if not cls.valid_location_name(location):
raise ValueError(f"{location} is not a valid boss location name.")
if location.split(" ")[-1] in ("top", "middle", "bottom"):
location = location.split(" ")
level = location[-1]
location = " ".join(location[:-1])
location = location.title().replace("Of", "of")
if not can_place_boss(boss.title(), location, level):
raise ValueError(f"{format_boss_location(location, level)} "
f"is not a valid location for {boss.title()}.")
else:
if not cls.valid_boss_name(option):
raise ValueError(f"{option} is not a valid boss name.")
@classmethod
def valid_boss_name(cls, value: str) -> bool:
return value.lower() in cls.bosses
@classmethod
def valid_location_name(cls, value: str) -> bool:
return value in cls.locations
def verify(self, world, player_name: str, plando_options) -> None:
if isinstance(self.value, int):
return
from Generate import PlandoSettings
if not(PlandoSettings.bosses & plando_options):
import logging
# plando is disabled but plando options were given so pull the option and change it to an int
option = self.value.split(";")[-1]
self.value = self.options[option]
logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} "
f"boss shuffle will be used for player {player_name}.")
class Enemies(Choice):
option_vanilla = 0
@@ -159,8 +287,6 @@ class Progressive(Choice):
option_off = 0
option_grouped_random = 1
option_on = 2
alias_false = 0
alias_true = 2
default = 2
def want_progressives(self, random):
@@ -168,8 +294,8 @@ class Progressive(Choice):
class Swordless(Toggle):
"""No swords. Curtains in Skull Woods and Agahnim\'s
Tower are removed, Agahnim\'s Tower barrier can be
"""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
@@ -202,8 +328,6 @@ class Hints(Choice):
option_on = 2
option_full = 3
default = 2
alias_false = 0
alias_true = 2
class Scams(Choice):
@@ -213,7 +337,6 @@ class Scams(Choice):
option_king_zora = 1
option_bottle_merchant = 2
option_all = 3
alias_false = 0
@property
def gives_king_zora_hint(self):
@@ -293,7 +416,6 @@ class HeartBeep(Choice):
option_half = 2
option_quarter = 3
option_off = 4
alias_false = 4
class HeartColor(Choice):
@@ -375,6 +497,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
"hints": Hints,
"scams": Scams,
"restrict_dungeon_item_on_boss": RestrictBossItem,
"boss_shuffle": Bosses,
"pot_shuffle": PotShuffle,
"enemy_shuffle": EnemyShuffle,
"killable_thieves": KillableThieves,

View File

@@ -4,6 +4,10 @@ import typing
from BaseClasses import Region, Entrance, RegionType
def is_main_entrance(entrance: Entrance) -> bool:
return entrance.parent_region.type in {RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic}
def create_regions(world, player):
world.regions += [

View File

@@ -12,7 +12,8 @@ from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .ItemPool import generate_itempool, difficulties
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
from .Options import alttp_options, smallkey_shuffle
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
is_main_entrance
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
get_hash_string, get_base_rom_path, LttPDeltaPatch
from .Rules import set_rules
@@ -24,6 +25,7 @@ lttp_logger = logging.getLogger("A Link to the Past")
extras_list = sum(difficulties['normal'].extras[0:5], [])
class ALTTPWeb(WebWorld):
setup_en = Tutorial(
"Multiworld Setup Tutorial",
@@ -349,7 +351,7 @@ class ALTTPWorld(World):
def use_enemizer(self):
world = self.world
player = self.player
return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
return (world.boss_shuffle[player] or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.pot_shuffle[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
@@ -410,6 +412,20 @@ class ALTTPWorld(World):
finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected
@classmethod
def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]):
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
world.shuffle[player] != "vanilla" or world.retro_caves[player]}
for region in world.regions:
if region.player in er_hint_data and region.locations:
main_entrance = region.get_connecting_entrance(is_main_entrance)
for location in region.locations:
if type(location.address) == int: # skips events and crystals
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][location.address] = main_entrance.name
hint_data.update(er_hint_data)
def modify_multidata(self, multidata: dict):
import base64
# wait for self.rom_name to be available.
@@ -424,8 +440,7 @@ class ALTTPWorld(World):
return ALttPItem(name, self.player, **item_init_table[name])
@classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
restitempool, fill_locations):
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
trash_counts = {}
standard_keyshuffle_players = set()
for player in world.get_game_players("A Link to the Past"):
@@ -472,26 +487,15 @@ class ALTTPWorld(World):
for player, trash_count in trash_counts.items():
gtower_locations = locations_mapping[player]
world.random.shuffle(gtower_locations)
localrest = localrestitempool[player]
if localrest:
gt_item_pool = restitempool + localrest
world.random.shuffle(gt_item_pool)
else:
gt_item_pool = restitempool.copy()
while gtower_locations and gt_item_pool and trash_count > 0:
while gtower_locations and filleritempool and trash_count > 0:
spot_to_fill = gtower_locations.pop()
item_to_place = gt_item_pool.pop()
item_to_place = filleritempool.pop()
if spot_to_fill.item_rule(item_to_place):
if item_to_place in localrest:
localrest.remove(item_to_place)
else:
restitempool.remove(item_to_place)
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill) # very slow, unfortunately
trash_count -= 1
def get_filler_item_name(self) -> str:
if self.world.goal[self.player] == "icerodhunt":
item = "Nothing"

View File

@@ -26,10 +26,14 @@
- Example: `Trinexx`
- Takes a particular boss and places that boss in any remaining slots in which this boss can function.
- In this example, it would fill Desert Palace, but not Tower of Hera.
- If no other options are provided this will follow normal singularity rules with that boss.
- Boss Shuffle:
- Example: `simple`
- Example: `basic`
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as
a last instruction.
- Supports `random` which will choose a random option from the normal choices.
- If one is not supplied any remaining locations will be unshuffled unless a single specific boss is
supplied in which case it will use singularity as noted above.
- [Available Bosses](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L135)
- [Available Arenas](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L150)

View File

@@ -14,7 +14,7 @@ import Patch
from . import Options
from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \
base_tech_table, tech_to_progressive_lookup, fluids
base_tech_table, tech_to_progressive_lookup, fluids, mods, tech_table, factorio_base_id
template_env: Optional[jinja2.Environment] = None
@@ -34,7 +34,9 @@ base_info = {
"factorio_version": "1.1",
"dependencies": [
"base >= 1.1.0",
"? science-not-invited"
"? science-not-invited",
"? factory-levels",
"! archipelago-extractor"
]
}
@@ -116,6 +118,10 @@ def generate_mod(world, output_directory: str):
return base - (base - low) * distance
return random.uniform(low, high)
all_items = tech_table.copy()
all_items["Attack Trap"] = factorio_base_id - 1
all_items["Evolution Trap"] = factorio_base_id - 2
template_data = {
"locations": locations, "player_names": multiworld.player_name, "tech_table": tech_table,
"base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup,
@@ -134,11 +140,13 @@ def generate_mod(world, output_directory: str):
"free_sample_blacklist": {item: 1 for item in free_sample_exclusions},
"progressive_technology_table": {tech.name: tech.progressive for tech in
progressive_technology_table.values()},
"item_id_to_name": {f"{item_id}": item_name for item_name, item_id in all_items.items()},
"custom_recipes": world.custom_recipes,
"max_science_pack": multiworld.max_science_pack[player].value,
"liquids": fluids,
"goal": multiworld.goal[player].value,
"energy_link": multiworld.energy_link[player].value
"energy_link": multiworld.energy_link[player].value,
"custom_data_package": 1 if mods else 0
}
for factorio_option in Options.factorio_options:
@@ -191,6 +199,8 @@ def generate_mod(world, output_directory: str):
f.write(locale_content)
info = base_info.copy()
info["name"] = mod_name
for mod in mods.values():
info["dependencies"].append(f"{mod.name} >= {mod.version}")
with open(os.path.join(mod_dir, "info.json"), "wt") as f:
json.dump(info, f, indent=4)

View File

@@ -137,8 +137,6 @@ class Progressive(Choice):
option_off = 0
option_grouped_random = 1
option_on = 2
alias_false = 0
alias_true = 2
default = 2
def want_progressives(self, random):

View File

@@ -4,6 +4,7 @@ import json
import logging
import os
import string
from sys import getrecursionlimit
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any
@@ -22,13 +23,14 @@ def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
import pkgutil
return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode())
# TODO: Make use of the lab information. (it has info on the science packs)
techs_future = pool.submit(load_json_data, "techs")
recipes_future = pool.submit(load_json_data, "recipes")
resources_future = pool.submit(load_json_data, "resources")
machines_future = pool.submit(load_json_data, "machines")
fluids_future = pool.submit(load_json_data, "fluids")
items_future = pool.submit(load_json_data, "items")
mods_future = pool.submit(load_json_data, "mods")
tech_table: Dict[str, int] = {}
technology_table: Dict[str, Technology] = {}
@@ -94,6 +96,8 @@ class CustomTechnology(Technology):
def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int):
ingredients = origin.ingredients & allowed_packs
if origin.ingredients and not ingredients:
logging.warning(f"Technology {origin.name} has no vanilla science packs. Custom science packs are not supported.")
military_allowed = "military-science-pack" in allowed_packs \
and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"})
or origin.name == "rocket-silo")
@@ -103,7 +107,8 @@ class CustomTechnology(Technology):
ingredients.add("military-science-pack")
ingredients = list(ingredients)
ingredients.sort() # deterministic sample
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
if ingredients:
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
elif origin.name == "rocket-silo" and military_allowed:
ingredients.add("military-science-pack")
super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id)
@@ -115,13 +120,19 @@ class Recipe(FactorioElement):
ingredients: Dict[str, int]
products: Dict[str, int]
energy: float
mining: bool
burning: bool
unlocked_at_start: bool
def __init__(self, name: str, category: str, ingredients: Dict[str, int], products: Dict[str, int], energy: float):
def __init__(self, name: str, category: str, ingredients: Dict[str, int], products: Dict[str, int], energy: float, mining: bool = False, unlocked_at_start: bool = False, burning: bool = False):
self.name = name
self.category = category
self.ingredients = ingredients
self.products = products
self.energy = energy
self.mining = mining
self.burning = burning
self.unlocked_at_start = unlocked_at_start
def __repr__(self):
return f"{self.__class__.__name__}({self.name})"
@@ -147,28 +158,56 @@ class Recipe(FactorioElement):
@property
def rel_cost(self) -> float:
ingredients = sum(self.ingredients.values())
return min(ingredients / amount for product, amount in self.products.items())
if all(amount == 0 for amount in self.products.values()):
return float('inf')
return min(ingredients / amount for product, amount in self.products.items() if amount > 0)
@property
def base_cost(self) -> Dict[str, int]:
ingredients = Counter()
for ingredient, cost in self.ingredients.items():
if ingredient in all_product_sources:
recipe_counters: Dict[str, (Recipe, Counter)] = {}
for recipe in all_product_sources[ingredient]:
recipe_ingredients = Counter()
if recipe.ingredients:
ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in
recipe_ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in
recipe.base_cost.items()})
else:
ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient]
recipe_ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient]
recipe_counters[recipe.name] = (recipe, recipe_ingredients)
selected_recipe_ingredients = None
for recipe_name, (recipe, recipe_ingredients) in recipe_counters.items():
if not selected_recipe_ingredients or (
sum([rel_cost.get(name, high_cost_item) * value for name, value in recipe_ingredients.items()])
< sum([rel_cost.get(name, high_cost_item) * value for name, value in selected_recipe_ingredients.items()])):
selected_recipe_ingredients = recipe_ingredients
ingredients.update(selected_recipe_ingredients)
else:
ingredients[ingredient] += cost
return ingredients
recursion_loop = 0
max_recursion_loop = 0.85 * getrecursionlimit()
def detect_recursive_loop(self) -> bool:
Recipe.recursion_loop += 1
if Recipe.max_recursion_loop < Recipe.recursion_loop:
Recipe.recursion_loop = 0
return True
for ingredient in self.ingredients.keys():
if ingredient in all_product_sources:
for ingredient_recipe in all_product_sources[ingredient]:
if ingredient_recipe.ingredients:
if ingredient_recipe.detect_recursive_loop():
return True
Recipe.recursion_loop -= 1
return False
@property
def total_energy(self) -> float:
"""Total required energy (crafting time) for single craft"""
# TODO: multiply mining energy by 2 since drill has 0.5 speed
total_energy = self.energy
total_energy = (self.energy / machines[self.crafting_machine].speed)
for ingredient, cost in self.ingredients.items():
if ingredient in all_product_sources:
selected_recipe_energy = float('inf')
@@ -182,10 +221,71 @@ class Recipe(FactorioElement):
class Machine(FactorioElement):
def __init__(self, name, categories):
def __init__(self, name, categories, machine_type, speed, item_sources, input_fluids, output_fluids):
self.name: str = name
self.categories: set = categories
self.machine_type: str = machine_type
self.speed: float = speed
self.item_sources: set = item_sources
self.input_fluids: int = int(input_fluids)
self.output_fluids: int = int(output_fluids)
class Lab(FactorioElement):
def __init__(self, name, inputs):
self.name: str = name
self.inputs: set = inputs
class Mod(FactorioElement):
def __init__(self, name, version):
self.name: str = name
self.version: str = version
class Item(FactorioElement):
def __init__(self, name, stack_size, stackable, place_result, burnt_result, fuel_value, fuel_category, rocket_launch_products):
self.name: str = name
self.stack_size: int = stack_size
self.stackable: bool = stackable
self.place_result: str = place_result
self.burnt_result: str = burnt_result
self.fuel_value: int = fuel_value
self.fuel_category: str = fuel_category
self.rocket_launch_products: Dict[str, int] = rocket_launch_products
self.product_sources: Set[Recipe] = set()
class Fluid(FactorioElement):
def __init__(self, name, default_temperature, max_temperature, heat_capacity):
self.name: str = name
if max_temperature == "inf":
max_temperature = 2**64
self.default_temperature: int = default_temperature
self.max_temperature: int = max_temperature
self.heat_capacity = heat_capacity
self.product_sources: Set[Recipe] = set()
items: Dict[str, Item] = {}
for name, item_data in items_future.result().items():
item = Item(name,
item_data.get("stack_size"),
item_data.get("stackable"),
item_data.get("place_result", None),
item_data.get("burnt_result", None),
item_data.get("fuel_value", 0),
item_data.get("fuel_category", None),
item_data.get("rocket_launch_products", {}))
items[name] = item
del items_future
fluids: Dict[str, Fluid] = {}
for name, fluid_data in fluids_future.result().items():
fluid = Fluid(name,
fluid_data.get("default_temperature", 0),
fluid_data.get("max_temperature", 0),
fluid_data.get("heat_capacity", 1000))
fluids[name] = fluid
del fluids_future
recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source
@@ -213,38 +313,149 @@ for resource_name, resource_data in resources_future.result().items():
if "required_fluid" in resource_data else {},
"products": {data["name"]: data["amount"] for data in resource_data["products"].values()},
"energy": resource_data["mining_time"],
"category": resource_data["category"]
"category": resource_data["category"],
"mining": True,
"unlocked_at_start": True
}
del resources_future
machines: Dict[str, Machine] = {}
labs: Dict[str, Lab] = {}
rel_cost = {}
high_cost_item = 10000
for name, prototype in machines_future.result().items():
machine_prototype = prototype.get("common", {}).get("type", None)
machine_item_sources = set(prototype.get("common", {}).get("placeable_by", {}))
for machine_type, machine_data in prototype.items():
if machine_type == "lab":
lab = Lab(name, machine_data.get("inputs", set()))
labs[name] = lab
if machine_type == "offshore-pump":
fluid = machine_data.get("fluid", None)
speed = machine_data.get("speed", None)
if not fluid or not speed:
continue
category = f"offshore-pumping-{fluid}-{speed}"
raw_recipes[category] = {
"ingredients": {},
"products": {fluid: (speed*60)},
"energy": 1,
"category": category,
"mining": True,
"unlocked_at_start": True
}
machine = Machine(name, {category}, machine_prototype, 1, machine_item_sources, 0, 1)
machines[name] = machine
if machine_type == "crafting":
categories = machine_data.get("categories", set())
if not categories:
continue
# TODO: Use speed / fluid_box info
speed = machine_data.get("speed", 1)
input_fluid_box = machine_data.get("input_fluid_box", 0)
output_fluid_box = machine_data.get("output_fluid_box", 0)
machine = Machine(name, set(categories), machine_prototype, speed, machine_item_sources, input_fluid_box, output_fluid_box)
machines[name] = machine
if machine_type == "mining":
categories = machine_data.get("categories", set())
if not categories:
continue
speed = machine_data.get("speed", 1)
input_fluid_box = machine_data.get("input_fluid_box", False) # Can this machine mine resources with required fluids?
output_fluid_box = machine_data.get("output_fluid_box", False) # Can this machine mine fluid resources?
machine = machines.setdefault(name, Machine(name, set(categories), machine_prototype, speed, machine_item_sources, input_fluid_box, output_fluid_box))
machine.categories |= set(categories) # character has both crafting and basic-solid
machine.speed = (machine.speed + speed) / 2
machines[name] = machine
if machine_type == "boiler":
input_fluid = machine_data.get("input_fluid")
output_fluid = machine_data.get("output_fluid")
target_temperature = machine_data.get("target_temperature")
energy_usage = machine_data.get("energy_usage")
amount = energy_usage / (target_temperature - fluids[input_fluid].default_temperature) / fluids[input_fluid].heat_capacity
amount *= 60
amount = int(amount)
category = f"boiling-{amount}-{input_fluid}-to-{output_fluid}-at-{target_temperature}-degrees-centigrade"
raw_recipes[category] = {
"ingredients": {input_fluid: amount},
"products": {output_fluid: amount},
"energy": 1,
"category": category,
"mining": True,
"unlocked_at_start": True
}
machine = Machine(name, {category}, machine_prototype, 1, machine_item_sources, 1, 1)
machines[name] = machine
if machine_type == "fuel_burner":
categories = set(machine_data.get("categories"))
fuel_items = {name: item for name, item in items.items() if item.burnt_result and item.fuel_category in categories}
if not fuel_items:
continue
energy_usage = machine_data.get("energy_usage")
for item_name, item in fuel_items.items():
recipe_name = f"burning-{item_name}-for-{item.burnt_result}"
raw_recipes[recipe_name] = {
"ingredients": {item_name: 1},
"products": {item.burnt_result: 1},
"energy": item.fuel_value,
"category": item.fuel_category,
"burning": True,
"unlocked_at_start": True
}
machine = machines.get(name, Machine(name, categories, machine_prototype, energy_usage * 60, machine_item_sources, 0, 0))
machines[name] = machine
# TODO: set up machine/recipe pairs for burners in order to retrieve the burnt_result from items.
# TODO: set up machine/recipe pairs for retrieving rocket_launch_products from items.
del machines_future
for recipe_name, recipe_data in raw_recipes.items():
# example:
# "accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting"}
# FIXME: add mining?
recipe = Recipe(recipe_name, recipe_data["category"], recipe_data["ingredients"],
recipe_data["products"], recipe_data["energy"] if "energy" in recipe_data else 0)
recipe = Recipe(recipe_name,
recipe_data["category"],
recipe_data["ingredients"],
recipe_data["products"],
recipe_data.get("energy", 0),
recipe_data.get("mining", False),
recipe_data.get("unlocked_at_start", False),
recipe_data.get("burning", False))
recipes[recipe_name] = recipe
if set(recipe.products).isdisjoint(
# prevents loop recipes like uranium centrifuging
set(recipe.ingredients)) and ("empty-barrel" not in recipe.products or recipe.name == "empty-barrel") and \
not recipe_name.endswith("-reprocessing"):
for product_name in recipe.products:
if set(recipe.products).isdisjoint(set(recipe.ingredients)):
for product_name in [product_name for product_name, amount in recipe.products.items() if amount > 0]:
all_product_sources.setdefault(product_name, set()).add(recipe)
if recipe.detect_recursive_loop():
# prevents loop recipes like uranium centrifuging and fluid unbarreling
all_product_sources.setdefault(product_name, set()).remove(recipe)
if not all_product_sources[product_name]:
del (all_product_sources[product_name])
if product_name in items:
items[product_name].product_sources.add(recipe)
if product_name in fluids:
fluids[product_name].product_sources.add(recipe)
machines: Dict[str, Machine] = {}
machines["assembling-machine-1"].categories |= machines["assembling-machine-3"].categories # mod enables this
machines["assembling-machine-2"].categories |= machines["assembling-machine-3"].categories
machines["rocket-silo"].input_fluids = 3
# machines["character"].categories.add("basic-crafting")
# charter only knows the categories of "crafting" and "basic-solid" by default.
for name, categories in machines_future.result().items():
machine = Machine(name, set(categories))
machines[name] = machine
# add electric mining drill as a crafting machine to resolve basic-solid (mining)
machines["electric-mining-drill"] = Machine("electric-mining-drill", {"basic-solid"})
machines["pumpjack"] = Machine("pumpjack", {"basic-fluid"})
machines["assembling-machine-1"].categories.add("crafting-with-fluid") # mod enables this
machines["character"].categories.add("basic-crafting") # somehow this is implied and not exported
mods: Dict[str, Mod] = {}
for name, version in mods_future.result().items():
if name in ["base", "archipelago-extractor"]:
continue
mod = Mod(name, version)
mods[name] = mod
del mods_future
del machines_future
# build requirements graph for all technology ingredients
@@ -254,7 +465,10 @@ for technology in technology_table.values():
def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]:
current_technologies = recipe.unlocking_technologies
if recipe.unlocked_at_start:
current_technologies = set()
else:
current_technologies = recipe.unlocking_technologies
for ingredient_name in recipe.ingredients:
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done,
unlock_func=unlock_just_tech)
@@ -262,7 +476,10 @@ def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]:
def unlock(recipe: Recipe, _done) -> Set[Technology]:
current_technologies = recipe.unlocking_technologies
if recipe.unlocked_at_start:
current_technologies = set()
else:
current_technologies = recipe.unlocking_technologies
for ingredient_name in recipe.ingredients:
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done, unlock_func=unlock)
current_technologies |= required_category_technologies[recipe.category]
@@ -289,21 +506,46 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_f
return current_technologies
for item_name, item in items.items():
if item.place_result and machines.get(item.place_result, None):
machines[item.place_result].item_sources |= {item_name}
required_machine_technologies: Dict[str, FrozenSet[Technology]] = {}
for ingredient_name in machines:
for ingredient_name, machine in machines.items():
if ingredient_name == "character":
required_machine_technologies[ingredient_name] = frozenset()
continue
techs = recursively_get_unlocking_technologies(ingredient_name)
for item_name in machine.item_sources:
techs |= recursively_get_unlocking_technologies(item_name)
required_machine_technologies[ingredient_name] = frozenset(techs)
for ingredient_name in labs:
required_machine_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name))
logical_machines = {}
machine_tech_cost = {}
for category in machines["character"].categories:
machine_tech_cost[category] = (10000, "character", machines["character"].speed)
for machine in machines.values():
if machine.name == "character":
continue
for category in machine.categories:
current_cost, current_machine = machine_tech_cost.get(category, (10000, "character"))
machine_cost = len(required_machine_technologies[machine.name])
if machine_cost < current_cost:
machine_tech_cost[category] = machine_cost, machine.name
if machine.machine_type == "character" and not machine_cost:
machine_cost = 10000
if category in machine_tech_cost:
current_cost, current_machine, current_speed = machine_tech_cost.get(category)
if machine_cost < current_cost or (machine_cost == current_cost and machine.speed > current_speed):
machine_tech_cost[category] = machine_cost, machine.name, machine.speed
else:
machine_tech_cost[category] = machine_cost, machine.name, machine.speed
machine_per_category: Dict[str: str] = {}
for category, (cost, machine_name) in machine_tech_cost.items():
for category, (cost, machine_name, speed) in machine_tech_cost.items():
machine_per_category[category] = machine_name
del machine_tech_cost
@@ -313,6 +555,10 @@ required_category_technologies: Dict[str, FrozenSet[FrozenSet[Technology]]] = {}
for category_name, machine_name in machine_per_category.items():
techs = set()
techs |= recursively_get_unlocking_technologies(machine_name)
if category_name in machines["character"].categories and techs:
# Character crafting/mining categories always have no tech assigned.
techs = set()
machine_per_category[category_name] = "character"
required_category_technologies[category_name] = frozenset(techs)
required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name: frozenset(
@@ -333,7 +579,10 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_
return {tech.name for tech in techs}
free_sample_exclusions: Set[str] = all_ingredient_names | {"rocket-part"}
free_sample_exclusions: Set[str] = all_ingredient_names.copy() # Rocket-silo crafting recipe results to be added here.
for name, recipe in recipes.items():
if machines[recipe.crafting_machine].machine_type == "rocket-silo":
free_sample_exclusions |= set(recipe.products)
# progressive technologies
# auto-progressive
@@ -447,37 +696,49 @@ useless_technologies: Set[str] = {tech_name for tech_name in common_tech_table
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
rel_cost = {
"wood": 10000,
"iron-ore": 1,
"copper-ore": 1,
"stone": 1,
"crude-oil": 0.5,
"water": 0.001,
"coal": 1,
"raw-fish": 1000,
"steam": 0.01,
"used-up-uranium-fuel-cell": 1000
}
for name, recipe in {name: recipe for name, recipe in recipes.items() if recipe.mining and not recipe.ingredients}.items():
machine = machines[machine_per_category[recipe.category]]
cost = recipe.energy / machine.speed
for product_name, amount in recipe.products.items():
rel_cost[product_name] = cost / amount
exclusion_list: Set[str] = all_ingredient_names | {"rocket-part", "used-up-uranium-fuel-cell"}
fluids: Set[str] = set(fluids_future.result())
del fluids_future
def get_estimated_difficulty(recipe: Recipe):
base_ingredients = recipe.base_cost
cost = 0
for ingredient_name, amount in base_ingredients.items():
cost += rel_cost.get(ingredient_name, high_cost_item) * amount
if recipe.burning:
for item in machines[recipe.crafting_machine].item_sources:
if items[item].product_sources:
cost += min([get_estimated_difficulty(recipe) for recipe in items[item].product_sources])
else:
cost += high_cost_item
# print(f"{recipe.name}: {cost} ({({ingredient_name: rel_cost.get(ingredient_name, high_cost_item) * amount for ingredient_name, amount in base_ingredients.items()})})")
return cost
for name, recipe in {name: recipe for name, recipe in recipes.items() if recipe.mining and recipe.ingredients}.items():
machine = machines[machine_per_category[recipe.category]]
cost = (recipe.energy / machine.speed) + get_estimated_difficulty(recipe)
for product_name, amount in recipe.products.items():
rel_cost[product_name] = cost / amount
exclusion_list: Set[str] = free_sample_exclusions.copy() # Also exclude the burnt results.
@Utils.cache_argsless
def get_science_pack_pools() -> Dict[str, Set[str]]:
def get_estimated_difficulty(recipe: Recipe):
base_ingredients = recipe.base_cost
cost = 0
for ingredient_name, amount in base_ingredients.items():
cost += rel_cost.get(ingredient_name, 1) * amount
return cost
science_pack_pools: Dict[str, Set[str]] = {}
already_taken = exclusion_list.copy()
current_difficulty = 5
unlocked_recipes = {name: recipe for name, recipe in recipes.items()
if recipe.unlocked_at_start
and not recipe.recursive_unlocking_technologies
and get_estimated_difficulty(recipe) < high_cost_item} # wood in recipe is expensive.
average_starting_difficulty = sum([get_estimated_difficulty(recipe) for name, recipe in unlocked_recipes.items()]) / len(unlocked_recipes)
current_difficulty = min(average_starting_difficulty, 8)
for science_pack in Options.MaxSciencePack.get_ordered_science_packs():
current = science_pack_pools[science_pack] = set()
for name, recipe in recipes.items():
@@ -487,9 +748,7 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
if science_pack == "automation-science-pack":
# Can't handcraft automation science if fluids end up in its recipe, making the seed impossible.
current -= fluids
elif science_pack == "logistic-science-pack":
current |= {"steam"}
current -= set(fluids)
current -= already_taken
already_taken |= current
@@ -498,10 +757,9 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
return science_pack_pools
item_stack_sizes: Dict[str, int] = items_future.result()
non_stacking_items: Set[str] = {item for item, stack in item_stack_sizes.items() if stack == 1}
stacking_items: Set[str] = set(item_stack_sizes) - non_stacking_items
valid_ingredients: Set[str] = stacking_items | fluids
non_stacking_items: Set[str] = {name for name, item in items.items() if not item.stackable}
stacking_items: Set[str] = set(items) - non_stacking_items
valid_ingredients: Set[str] = stacking_items | set(fluids)
# cleanup async helpers
pool.shutdown()

View File

@@ -8,7 +8,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
fluids, stacking_items, valid_ingredients
fluids, stacking_items, valid_ingredients, mods
from .Shapes import get_shapes
from .Mod import generate_mod
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal
@@ -54,8 +54,8 @@ class Factorio(World):
item_name_groups = {
"Progressive": set(progressive_tech_table.keys()),
}
data_version = 5
required_client_version = (0, 3, 0)
data_version = 0 if mods else 5
required_client_version = (0, 3, 5) if mods else (0, 3, 0) # TODO: Update required_client_version to (0, 3, 6) when that version releases.
def __init__(self, world, player: int):
super(Factorio, self).__init__(world, player)
@@ -224,7 +224,7 @@ class Factorio(World):
liquids_used += 1 if new_ingredient in fluids else 0
new_ingredients[new_ingredient] = 1
return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients,
original.products, original.energy)
original.products, original.energy, original.mining, original.unlocked_at_start)
def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: float = 1,
allow_liquids: int = 2) -> Recipe:
@@ -258,6 +258,8 @@ class Factorio(World):
ingredient_energy = 2
if not ingredient_raw:
ingredient_raw = 1
if not ingredient_energy:
ingredient_energy = 1/60
if remaining_num_ingredients == 1:
max_raw = 1.1 * remaining_raw
min_raw = 0.9 * remaining_raw
@@ -293,13 +295,18 @@ class Factorio(World):
continue # can't use this ingredient as we already have maximum liquid in our recipe.
ingredient_recipe = recipes.get(ingredient, None)
if not ingredient_recipe and ingredient.endswith("-barrel"):
ingredient_recipe = recipes.get(f"fill-{ingredient}", None)
if not ingredient_recipe and ingredient in all_product_sources:
ingredient_recipe = min(all_product_sources[ingredient], key=lambda recipe: recipe.rel_cost)
if not ingredient_recipe:
logging.warning(f"missing recipe for {ingredient}")
continue
ingredient_raw = sum((count for ingredient, count in ingredient_recipe.base_cost.items()))
ingredient_energy = ingredient_recipe.total_energy
if not ingredient_raw:
ingredient_raw = 1
if not ingredient_energy:
ingredient_energy = 1/60
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))
@@ -317,7 +324,7 @@ class Factorio(World):
logging.warning("could not randomize recipe")
return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients,
original.products, original.energy)
original.products, original.energy, original.mining, original.unlocked_at_start)
def set_custom_technologies(self):
custom_technologies = {}
@@ -330,11 +337,20 @@ class Factorio(World):
original_rocket_part = recipes["rocket-part"]
science_pack_pools = get_science_pack_pools()
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
if len(valid_pool) < 3:
# Not enough items in max pool. Extend to entire pool.
valid_pool = []
for pack in self.world.max_science_pack[self.player].get_ordered_science_packs():
valid_pool += sorted(science_pack_pools[pack] & valid_ingredients)
if len(valid_pool) < 3:
raise Exception("Not enough ingredients available for generation.")
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)},
original_rocket_part.products,
original_rocket_part.energy)}
original_rocket_part.energy,
original_rocket_part.mining,
original_rocket_part.unlocked_at_start)}
if self.world.recipe_ingredients[self.player]:
valid_pool = []
@@ -363,7 +379,7 @@ class Factorio(World):
bridge = "ap-energy-bridge"
new_recipe = self.make_quick_recipe(
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1},
{bridge: 1}, 10),
{bridge: 1}, 10, False, self.world.energy_link[self.player].value),
sorted(science_pack_pools[self.world.max_science_pack[self.player].get_ordered_science_packs()[0]]))
for ingredient_name in new_recipe.ingredients:
new_recipe.ingredients[ingredient_name] = self.world.random.randint(10, 100)

View File

@@ -1 +1 @@
["fluid-unknown","water","crude-oil","steam","heavy-oil","light-oil","petroleum-gas","sulfuric-acid","lubricant"]
{"fluid-unknown":{"default_temperature":0,"max_temperature":0,"heat_capacity":1000},"water":{"default_temperature":15,"max_temperature":100,"heat_capacity":200},"crude-oil":{"default_temperature":25,"max_temperature":25,"heat_capacity":100},"steam":{"default_temperature":15,"max_temperature":1000,"heat_capacity":200},"heavy-oil":{"default_temperature":25,"max_temperature":25,"heat_capacity":100},"light-oil":{"default_temperature":25,"max_temperature":25,"heat_capacity":100},"petroleum-gas":{"default_temperature":25,"max_temperature":25,"heat_capacity":100},"sulfuric-acid":{"default_temperature":25,"max_temperature":25,"heat_capacity":100},"lubricant":{"default_temperature":25,"max_temperature":25,"heat_capacity":100}}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"oil-refinery":{"oil-processing":true},"chemical-plant":{"chemistry":true},"centrifuge":{"centrifuging":true},"rocket-silo":{"rocket-building":true},"character":{"crafting":true}}
{"boiler":{"boiler":{"input_fluid":"water","output_fluid":"steam","target_temperature":165,"energy_usage":30000},"common":{"type":"boiler","placeable_by":{"boiler":true}}},"nuclear-reactor":{"fuel_burner":{"categories":{"nuclear":true},"energy_usage":666666.666666666627861559391021728515625},"common":{"type":"reactor","placeable_by":{"nuclear-reactor":true}}},"heat-exchanger":{"boiler":{"input_fluid":"water","output_fluid":"steam","target_temperature":500,"energy_usage":166666.66666666665696538984775543212890625},"common":{"type":"boiler","placeable_by":{"heat-exchanger":true}}},"burner-mining-drill":{"mining":{"categories":{"basic-solid":true},"speed":0.25,"input_fluid_box":false,"output_fluid_box":false},"common":{"type":"mining-drill","placeable_by":{"burner-mining-drill":true}}},"electric-mining-drill":{"mining":{"categories":{"basic-solid":true},"speed":0.5,"input_fluid_box":true,"output_fluid_box":false},"common":{"type":"mining-drill","placeable_by":{"electric-mining-drill":true}}},"offshore-pump":{"offshore-pump":{"fluid":"water","speed":20},"common":{"type":"offshore-pump","placeable_by":{"offshore-pump":true}}},"pumpjack":{"mining":{"categories":{"basic-fluid":true},"speed":1,"input_fluid_box":false,"output_fluid_box":true},"common":{"type":"mining-drill","placeable_by":{"pumpjack":true}}},"stone-furnace":{"crafting":{"speed":1,"categories":{"smelting":true},"input_fluid_box":0,"output_fluid_box":0},"common":{"type":"furnace","placeable_by":{"stone-furnace":true}}},"steel-furnace":{"crafting":{"speed":2,"categories":{"smelting":true},"input_fluid_box":0,"output_fluid_box":0},"common":{"type":"furnace","placeable_by":{"steel-furnace":true}}},"electric-furnace":{"crafting":{"speed":2,"categories":{"smelting":true},"input_fluid_box":0,"output_fluid_box":0},"common":{"type":"furnace","placeable_by":{"electric-furnace":true}}},"assembling-machine-1":{"crafting":{"speed":0.5,"categories":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"input_fluid_box":0,"output_fluid_box":0},"common":{"type":"assembling-machine","placeable_by":{"assembling-machine-1":true}}},"assembling-machine-2":{"crafting":{"speed":0.75,"categories":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"input_fluid_box":1,"output_fluid_box":1},"common":{"type":"assembling-machine","placeable_by":{"assembling-machine-2":true}}},"assembling-machine-3":{"crafting":{"speed":1.25,"categories":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"input_fluid_box":1,"output_fluid_box":1},"common":{"type":"assembling-machine","placeable_by":{"assembling-machine-3":true}}},"oil-refinery":{"crafting":{"speed":1,"categories":{"oil-processing":true},"input_fluid_box":2,"output_fluid_box":3},"common":{"type":"assembling-machine","placeable_by":{"oil-refinery":true}}},"chemical-plant":{"crafting":{"speed":1,"categories":{"chemistry":true},"input_fluid_box":2,"output_fluid_box":2},"common":{"type":"assembling-machine","placeable_by":{"chemical-plant":true}}},"centrifuge":{"crafting":{"speed":1,"categories":{"centrifuging":true},"input_fluid_box":0,"output_fluid_box":0},"common":{"type":"assembling-machine","placeable_by":{"centrifuge":true}}},"lab":{"lab":{"inputs":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","production-science-pack","utility-science-pack","space-science-pack"]},"common":{"type":"lab","placeable_by":{"lab":true}}},"rocket-silo":{"crafting":{"speed":1,"categories":{"rocket-building":true},"fixed_recipe":"rocket-part","input_fluid_box":0,"output_fluid_box":0},"common":{"type":"rocket-silo","placeable_by":{"rocket-silo":true}}},"character":{"crafting":{"speed":1,"categories":{"crafting":true},"input_fluid_box":0,"output_fluid_box":0},"mining":{"categories":{"basic-solid":true},"speed":0.5,"input_fluid_box":false,"output_fluid_box":false},"common":{"type":"character"}}}

View File

@@ -7,7 +7,9 @@
"description": "Integration client for the Archipelago Randomizer",
"factorio_version": "1.1",
"dependencies": [
"base >= 1.1.0",
"? science-not-invited"
]
"base >= 1.1.0",
"? science-not-invited",
"? factory-levels",
"! archipelago-extractor"
]
}

View File

@@ -13,6 +13,7 @@ GOAL = {{ goal }}
ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}"
ENERGY_INCREMENT = {{ energy_link * 1000000 }}
ENERGY_LINK_EFFICIENCY = 0.75
CUSTOM_DATA_PACKAGE = {{ custom_data_package }}
if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then
DEATH_LINK = 1
@@ -515,6 +516,10 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
local item_name = chunks[1]
local index = chunks[2]
local source = chunks[3] or "Archipelago"
if CUSTOM_DATA_PACKAGE == 1 then
item_name = item_id_to_name[item_name] or item_name
end
if index == -1 then -- for coop sync and restoring from an older savegame
tech = force.technologies[item_name]
if tech.researched ~= true then
@@ -571,7 +576,8 @@ commands.add_command("ap-rcon-info", "Used by the Archipelago client to get info
["slot_name"] = SLOT_NAME,
["seed_name"] = SEED_NAME,
["death_link"] = DEATH_LINK,
["energy_link"] = ENERGY_INCREMENT
["energy_link"] = ENERGY_INCREMENT,
["custom_data_package"] = CUSTOM_DATA_PACKAGE
}))
end)
@@ -598,3 +604,4 @@ end)
-- data
progressive_technologies = {{ dict_to_lua(progressive_technology_table) }}
item_id_to_name = {{ dict_to_lua(item_id_to_name) }}

View File

@@ -141,6 +141,9 @@ end
{# This got complex, but seems to be required to hit all corner cases #}
function adjust_energy(recipe_name, factor)
local recipe = data.raw.recipe[recipe_name]
if recipe == nil then
error("Some mod that is installed has removed recipe \"" .. recipe_name .. "\"")
end
local energy = recipe.energy_required
if (recipe.normal ~= nil) then
@@ -168,6 +171,9 @@ end
function set_energy(recipe_name, energy)
local recipe = data.raw.recipe[recipe_name]
if recipe == nil then
error("Some mod that is installed has removed recipe \"" .. recipe_name .. "\"")
end
if (recipe.normal ~= nil) then
recipe.normal.energy_required = energy
@@ -183,11 +189,26 @@ end
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
if mods["factory-levels"] then
-- Factory-Levels allows the assembling machines to get faster (and depending on settings), more productive at crafting products, the more the
-- assembling machine crafts the product. If the machine crafts enough, it may auto-upgrade to the next tier.
for i = 1, 25, 1 do
data.raw["assembling-machine"]["assembling-machine-1-level-" .. i].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
data.raw["assembling-machine"]["assembling-machine-1-level-" .. i].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
end
for i = 1, 50, 1 do
data.raw["assembling-machine"]["assembling-machine-2-level-" .. i].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
end
end
data.raw["ammo"]["artillery-shell"].stack_size = 10
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
{%- for original_tech_name, item_name, receiving_player, advancement in locations %}
original_tech = technologies["{{original_tech_name}}"]
if original_tech == nil then
error("Some mod that is installed has removed tech \"{{original_tech_name}}\"")
end
{#- the tech researched by the local player #}
new_tree_copy = table.deepcopy(template_tech)
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
@@ -220,13 +241,13 @@ data:extend{new_tree_copy}
{% endfor %}
{% if recipe_time_scale %}
{%- for recipe_name, recipe in recipes.items() %}
{%- if recipe.category not in ("basic-solid", "basic-fluid") %}
{%- if not recipe.mining and not recipe.burning %}
adjust_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_scale) }})
{%- endif %}
{%- endfor -%}
{% elif recipe_time_range %}
{%- for recipe_name, recipe in recipes.items() %}
{%- if recipe.category not in ("basic-solid", "basic-fluid") %}
{%- if not recipe.mining and not recipe.burning %}
set_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_range) }})
{%- endif %}
{%- endfor -%}

View File

@@ -0,0 +1 @@
{"base":"1.1.69","archipelago-extractor":"1.1.1"}

File diff suppressed because one or more lines are too long

View File

@@ -66,7 +66,10 @@ class FF1World(World):
def goal_rule_and_shards(state):
return goal_rule(state) and state.has("Shard", self.player, 32)
terminated_event.access_rule = goal_rule_and_shards
if "MARK" in items.keys():
# Fail generation for Noverworld and provide link to old FFR website
raise Exception("FFR Noverworld seeds must be generated on an older version of FFR. Please ensure you generated the settings using "
"4-4-0.finalfantasyrandomizer.com")
menu_region.locations.append(terminated_event)
self.world.regions += [menu_region]

View File

@@ -409,7 +409,6 @@ class DeathLink(Choice):
shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise.
"""
option_off = 0
alias_false = 0
alias_no = 0
alias_true = 1
alias_on = 1
@@ -435,10 +434,8 @@ class CostSanity(Choice):
These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
"""
option_off = 0
alias_false = 0
alias_no = 0
option_on = 1
alias_true = 1
alias_yes = 1
option_shopsonly = 2
option_notshops = 3

View File

@@ -101,7 +101,6 @@ class InteriorEntrances(Choice):
option_off = 0
option_simple = 1
option_all = 2
alias_false = 0
alias_true = 2
@@ -141,7 +140,6 @@ class MixEntrancePools(Choice):
option_off = 0
option_indoor = 1
option_all = 2
alias_false = 0
class DecoupleEntrances(Toggle):
@@ -308,7 +306,6 @@ class ShopShuffle(Choice):
option_off = 0
option_fixed_number = 1
option_random_number = 2
alias_false = 0
class ShopSlots(Range):
@@ -326,7 +323,6 @@ class TokenShuffle(Choice):
option_dungeons = 1
option_overworld = 2
option_all = 3
alias_false = 0
class ScrubShuffle(Choice):
@@ -336,7 +332,6 @@ class ScrubShuffle(Choice):
option_low = 1
option_regular = 2
option_random_prices = 3
alias_false = 0
alias_affordable = 1
alias_expensive = 2
@@ -569,7 +564,6 @@ class Hints(Choice):
option_agony = 2
option_always = 3
default = 3
alias_false = 0
class MiscHints(DefaultOnToggle):
@@ -673,8 +667,6 @@ class IceTraps(Choice):
option_mayhem = 3
option_onslaught = 4
default = 1
alias_false = 0
alias_true = 2
alias_extra = 2
@@ -742,7 +734,6 @@ class Music(Choice):
option_normal = 0
option_off = 1
option_randomized = 2
alias_false = 1
class BackgroundMusic(Music):

View File

@@ -282,8 +282,7 @@ class SA2BWorld(World):
spoiler_handle.writelines(text)
@classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
restitempool, fill_locations):
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
if world.get_game_players("Sonic Adventure 2 Battle"):
progitempool.sort(
key=lambda item: 0 if (item.name != 'Emblem') else 1)

View File

@@ -163,3 +163,17 @@ filler_items: typing.Tuple[str, ...] = (
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
data.code}
# Map type to expected int
type_flaggroups: typing.Dict[str, int] = {
"Unit": 0,
"Upgrade": 1,
"Armory 1": 2,
"Armory 2": 3,
"Building": 4,
"Mercenary": 5,
"Laboratory": 6,
"Protoss": 7,
"Minerals": 8,
"Vespene": 9,
"Supply": 10,
}

View File

@@ -122,8 +122,6 @@ class AreaRandomization(Choice):
option_off = 0
option_light = 1
option_on = 2
alias_false = 0
alias_true = 2
default = 0
class AreaLayout(Toggle):

View File

@@ -660,8 +660,7 @@ class SMWorld(World):
loc.address = loc.item.code = None
@classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
restitempool, fill_locations):
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
if world.get_game_players("Super Metroid"):
progitempool.sort(
key=lambda item: 1 if (item.name == 'Morph Ball') else 0)

View File

@@ -1,48 +1,57 @@
import typing
from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice
class EnableCoinStars(DefaultOnToggle):
"""Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything"""
display_name = "Enable 100 Coin Stars"
class StrictCapRequirements(DefaultOnToggle):
"""If disabled, Stars that expect special caps may have to be acquired without the caps"""
display_name = "Strict Cap Requirements"
class StrictCannonRequirements(DefaultOnToggle):
"""If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy Checks are enabled"""
display_name = "Strict Cannon Requirements"
class FirstBowserStarDoorCost(Range):
"""How many stars are required at the Star Door to Bowser in the Dark World"""
range_start = 0
range_end = 50
default = 8
class BasementStarDoorCost(Range):
"""How many stars are required at the Star Door in the Basement"""
range_start = 0
range_end = 70
default = 30
class SecondFloorStarDoorCost(Range):
"""How many stars are required to access the third floor"""
range_start = 0
range_end = 90
default = 50
class MIPS1Cost(Range):
"""How many stars are required to spawn MIPS the first time"""
range_start = 0
range_end = 40
default = 15
class MIPS2Cost(Range):
"""How many stars are required to spawn MIPS the secound time. Must be bigger or equal MIPS1Cost"""
range_start = 0
range_end = 80
default = 50
class StarsToFinish(Range):
"""How many stars are required at the infinite stairs"""
display_name = "Endless Stairs Stars"
@@ -50,35 +59,40 @@ class StarsToFinish(Range):
range_end = 100
default = 70
class AmountOfStars(Range):
"""How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set"""
range_start = 35
range_end = 120
default = 120
class AreaRandomizer(Choice):
"""Randomize Entrances"""
display_name = "Entrance Randomizer"
alias_false = 0
option_Off = 0
option_Courses_Only = 1
option_Courses_and_Secrets = 2
class BuddyChecks(Toggle):
"""Bob-omb Buddies are checks, Cannon Unlocks are items"""
display_name = "Bob-omb Buddy Checks"
class ExclamationBoxes(Choice):
"""Include 1Up Exclamation Boxes during randomization"""
display_name = "Randomize 1Up !-Blocks"
option_Off = 0
option_1Ups_Only = 1
class ProgressiveKeys(DefaultOnToggle):
"""Keys will first grant you access to the Basement, then to the Secound Floor"""
display_name = "Progressive Keys"
sm64_options: typing.Dict[str,type(Option)] = {
sm64_options: typing.Dict[str, type(Option)] = {
"AreaRandomizer": AreaRandomizer,
"ProgressiveKeys": ProgressiveKeys,
"EnableCoinStars": EnableCoinStars,
@@ -93,5 +107,5 @@ sm64_options: typing.Dict[str,type(Option)] = {
"StarsToFinish": StarsToFinish,
"death_link": DeathLink,
"BuddyChecks": BuddyChecks,
"ExclamationBoxes": ExclamationBoxes
}
"ExclamationBoxes": ExclamationBoxes,
}

View File

@@ -86,6 +86,7 @@ def set_rules(world, player: int, area_connections):
# which would make it impossible to reach downtown area without the cannon.
add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Cannon Unlock WDW", player))
add_rule(world.get_location("WDW: Go to Town for Red Coins", player), lambda state: state.has("Cannon Unlock WDW", player))
add_rule(world.get_location("WDW: 1Up Block in Downtown", player), lambda state: state.has("Cannon Unlock WDW", player))
if world.StrictCapRequirements[player]:
add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Wing Cap", player))

View File

@@ -9,6 +9,7 @@ from .Regions import create_regions, sm64courses, sm64entrances_s, sm64_internal
from BaseClasses import Item, Tutorial, ItemClassification
from ..AutoWorld import World, WebWorld
class SM64Web(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",

View File

@@ -107,7 +107,6 @@ class HeartBeepSpeed(Choice):
option_Half = 2
option_Normal = 3
option_Double = 4
alias_false = 0
default = 3
class HeartColor(Choice):

View File

@@ -21,8 +21,6 @@ class OffOnFullChoice(Choice):
option_on = 1
option_full = 2
alias_chaos = 2
alias_false = 0
alias_true = 1
class Difficulty(EvermizerFlags, Choice):

View File

@@ -139,7 +139,7 @@ item_table: Dict[int, ItemDict] = {
'name': 'Power Transmitter Fragment',
'tech_type': 'PowerTransmitterFragment'},
35032: {'classification': ItemClassification.progression,
'count': 4,
'count': 5,
'name': 'Prawn Suit Fragment',
'tech_type': 'ExosuitFragment'},
35033: {'classification': ItemClassification.useful,
@@ -163,7 +163,7 @@ item_table: Dict[int, ItemDict] = {
'name': 'Scanner Room Fragment',
'tech_type': 'BaseMapRoomFragment'},
35038: {'classification': ItemClassification.progression,
'count': 5,
'count': 4,
'name': 'Seamoth Fragment',
'tech_type': 'SeamothFragment'},
35039: {'classification': ItemClassification.progression,
@@ -203,9 +203,9 @@ item_table: Dict[int, ItemDict] = {
'name': 'Picture Frame',
'tech_type': 'PictureFrameFragment'},
35048: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Bench Fragment',
'tech_type': 'BenchFragment'},
'count': 1,
'name': 'Bench',
'tech_type': 'Bench'},
35049: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Basic Plant Pot',
@@ -333,7 +333,12 @@ item_table: Dict[int, ItemDict] = {
35080: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Water Filtration Machine',
'tech_type': 'BaseFiltrationMachine'}}
'tech_type': 'BaseFiltrationMachine'},
35081: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Ultra High Capacity Tank',
'tech_type': 'HighCapacityTank'},
}
advancement_item_names: Set[str] = set()
non_advancement_item_names: Set[str] = set()

View File

@@ -41,7 +41,7 @@ class SubnauticaWorld(World):
location_name_to_id = all_locations
option_definitions = Options.options
data_version = 6
data_version = 7
required_client_version = (0, 3, 5)
prefill_items: List[Item]

View File

@@ -3,66 +3,82 @@ from BaseClasses import MultiWorld
from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict
from schema import Schema, And, Optional
class StartWithJewelryBox(Toggle):
"Start with Jewelry Box unlocked"
display_name = "Start with Jewelry Box"
#class ProgressiveVerticalMovement(Toggle):
# "Always find vertical movement in the following order Succubus Hairpin -> Light Wall -> Celestial Sash"
# display_name = "Progressive vertical movement"
#class ProgressiveKeycards(Toggle):
# "Always find Security Keycard's in the following order D -> C -> B -> A"
# display_name = "Progressive keycards"
class DownloadableItems(DefaultOnToggle):
"With the tablet you will be able to download items at terminals"
display_name = "Downloadable items"
class EyeSpy(Toggle):
"Requires Oculus Ring in inventory to be able to break hidden walls."
display_name = "Eye Spy"
class StartWithMeyef(Toggle):
"Start with Meyef, ideal for when you want to play multiplayer."
display_name = "Start with Meyef"
class QuickSeed(Toggle):
"Start with Talaria Attachment, Nyoom!"
display_name = "Quick seed"
class SpecificKeycards(Toggle):
"Keycards can only open corresponding doors"
display_name = "Specific Keycards"
class Inverted(Toggle):
"Start in the past"
display_name = "Inverted"
#class StinkyMaw(Toggle):
# "Require gasmask for Maw"
# display_name = "Stinky Maw"
class GyreArchives(Toggle):
"Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo"
display_name = "Gyre Archives"
class Cantoran(Toggle):
"Cantoran's fight and check are available upon revisiting his room"
display_name = "Cantoran"
class LoreChecks(Toggle):
"Memories and journal entries contain items."
display_name = "Lore Checks"
class BossRando(Toggle):
"Shuffles the positions of all bosses."
display_name = "Boss Randomization"
class BossScaling(DefaultOnToggle):
"When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Reccomended)"
display_name = "Scale Random Boss Stats"
class DamageRando(Choice):
"Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings."
display_name = "Damage Rando"
@@ -73,9 +89,9 @@ class DamageRando(Choice):
option_mostlybuffs = 4
option_allbuffs = 5
option_manual = 6
alias_false = 0
alias_true = 2
class DamageRandoOverrides(OptionDict):
"Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that you don't specify will roll with 1/1/1 as odds"
schema = Schema({
@@ -180,6 +196,7 @@ class DamageRandoOverrides(OptionDict):
"Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
}
class HpCap(Range):
"Sets the number that Lunais's HP maxes out at."
display_name = "HP Cap"
@@ -187,10 +204,12 @@ class HpCap(Range):
range_end = 999
default = 999
class BossHealing(DefaultOnToggle):
"Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled."
display_name = "Heal After Bosses"
class ShopFill(Choice):
"""Sets the items for sale in Merchant Crow's shops.
Default: No sunglasses or trendy jacket, but sand vials for sale.
@@ -203,10 +222,12 @@ class ShopFill(Choice):
option_vanilla = 2
option_empty = 3
class ShopWarpShards(DefaultOnToggle):
"Shops always sell warp shards (when keys possessed), ignoring inventory setting."
display_name = "Always Sell Warp Shards"
class ShopMultiplier(Range):
"Multiplier for the cost of items in the shop. Set to 0 for free shops."
display_name = "Shop Price Multiplier"
@@ -214,6 +235,7 @@ class ShopMultiplier(Range):
range_end = 10
default = 1
class LootPool(Choice):
"""Sets the items that drop from enemies (does not apply to boss reward checks)
Vanilla: Drops are the same as the base game
@@ -224,6 +246,7 @@ class LootPool(Choice):
option_randomized = 1
option_empty = 2
class DropRateCategory(Choice):
"""Sets the drop rate when 'Loot Pool' is set to 'Random'
Tiered: Based on item rarity/value
@@ -237,6 +260,7 @@ class DropRateCategory(Choice):
option_randomized = 2
option_fixed = 3
class FixedDropRate(Range):
"Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'"
display_name = "Fixed Drop Rate"
@@ -244,6 +268,7 @@ class FixedDropRate(Range):
range_end = 100
default = 5
class LootTierDistro(Choice):
"""Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random'
Default Weight: Rarer items will be assigned to enemy drop slots less frequently than common items
@@ -255,14 +280,17 @@ class LootTierDistro(Choice):
option_full_random = 1
option_inverted_weight = 2
class ShowBestiary(Toggle):
"All entries in the bestiary are visible, without needing to kill one of a given enemy first"
display_name = "Show Bestiary Entries"
class ShowDrops(Toggle):
"All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first"
display_name = "Show Bestiary Item Drops"
# Some options that are available in the timespinner randomizer arent currently implemented
timespinner_options: Dict[str, Option] = {
"StartWithJewelryBox": StartWithJewelryBox,
@@ -296,9 +324,11 @@ timespinner_options: Dict[str, Option] = {
"DeathLink": DeathLink,
}
def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool:
return get_option_value(world, player, name) > 0
def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, dict]:
option = getattr(world, name, None)
if option == None:

View File

@@ -15,9 +15,9 @@ class DisableNonRandomizedPuzzles(DefaultOnToggle):
class EarlySecretArea(Toggle):
"""Opens the Mountainside shortcut to the Mountain Secret Area from the start.
"""Opens the Mountainside shortcut to the Caves from the start.
(Otherwise known as "UTM", "Caves" or the "Challenge Area")"""
display_name = "Early Secret Area"
display_name = "Early Caves"
class ShuffleSymbols(DefaultOnToggle):
@@ -58,15 +58,9 @@ class ShuffleVaultBoxes(Toggle):
display_name = "Shuffle Vault Boxes"
class ShuffleUncommonLocations(Toggle):
"""Adds some optional puzzles that are somewhat difficult or out of the way.
Examples: Mountaintop River Shape, Tutorial Patio Floor, Theater Videos"""
display_name = "Shuffle Uncommon Locations"
class ShufflePostgame(Toggle):
"""Adds locations into the pool that are guaranteed to be locked behind your goal. Use this if you don't play with
forfeit on victory."""
"""Adds locations into the pool that are guaranteed to become accessible before or at the same time as your goal.
Use this if you don't play with forfeit on victory."""
display_name = "Shuffle Postgame"
@@ -90,7 +84,7 @@ class MountainLasers(Range):
class ChallengeLasers(Range):
"""Sets the amount of beams required to enter the secret area through the Mountain Bottom Layer Discard."""
"""Sets the amount of beams required to enter the Caves through the Mountain Bottom Floor Discard."""
display_name = "Required Lasers for Challenge"
range_start = 1
range_end = 11
@@ -122,7 +116,6 @@ the_witness_options: Dict[str, type] = {
"disable_non_randomized_puzzles": DisableNonRandomizedPuzzles,
"shuffle_discarded_panels": ShuffleDiscardedPanels,
"shuffle_vault_boxes": ShuffleVaultBoxes,
"shuffle_uncommon": ShuffleUncommonLocations,
"shuffle_postgame": ShufflePostgame,
"victory_condition": VictoryCondition,
"mountain_lasers": MountainLasers,

View File

@@ -28,38 +28,38 @@ Traps:
610 - Power Surge
Doors:
1100 - Glass Factory Entry Door (Panel) - 0x01A54
1105 - Door to Symmetry Island Lower (Panel) - 0x000B0
1107 - Door to Symmetry Island Upper (Panel) - 0x1C349
1110 - Door to Desert Flood Light Room (Panel) - 0x0C339
1111 - Desert Flood Room Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B
1119 - Quarry Door to Mill (Panel) - 0x01E5A,0x01E59
1100 - Glass Factory Entry (Panel) - 0x01A54
1105 - Symmetry Island Lower (Panel) - 0x000B0
1107 - Symmetry Island Upper (Panel) - 0x1C349
1110 - Desert Light Room Entry (Panel) - 0x0C339
1111 - Desert Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B
1119 - Quarry Mill Entry (Panel) - 0x01E5A,0x01E59
1120 - Quarry Mill Ramp Controls (Panel) - 0x03678,0x03676
1122 - Quarry Mill Elevator Controls (Panel) - 0x03679,0x03675
1122 - Quarry Mill Lift Controls (Panel) - 0x03679,0x03675
1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852
1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858
1131 - Shadows Door Timer (Panel) - 0x334DB,0x334DC
1150 - Monastery Entry Door Left (Panel) - 0x00B10
1151 - Monastery Entry Door Right (Panel) - 0x00C92
1162 - Town Door to RGB House (Panel) - 0x28998
1163 - Town Door to Church (Panel) - 0x28A0D
1150 - Monastery Entry Left (Panel) - 0x00B10
1151 - Monastery Entry Right (Panel) - 0x00C92
1162 - Town Tinted Glass Door (Panel) - 0x28998
1163 - Town Church Entry (Panel) - 0x28A0D
1166 - Town Maze Panel (Drop-Down Staircase) (Panel) - 0x28A79
1169 - Windmill Door (Panel) - 0x17F5F
1169 - Windmill Entry (Panel) - 0x17F5F
1200 - Treehouse First & Second Doors (Panel) - 0x0288C,0x02886
1202 - Treehouse Third Door (Panel) - 0x0A182
1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x334DC
1208 - Treehouse Shortcut Drop-Down Bridge (Panel) - 0x17CBC
1208 - Treehouse Drawbridge (Panel) - 0x17CBC
1175 - Jungle Popup Wall (Panel) - 0x17CAB
1180 - Bunker Entry Door (Panel) - 0x17C2E
1183 - Inside Bunker Door to Bunker Proper (Panel) - 0x0A099
1180 - Bunker Entry (Panel) - 0x17C2E
1183 - Bunker Tinted Glass Door (Panel) - 0x0A099
1186 - Bunker Elevator Control (Panel) - 0x0A079
1190 - Swamp Entry Door (Panel) - 0x0056E
1190 - Swamp Entry (Panel) - 0x0056E
1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488
1195 - Swamp Rotating Bridge (Panel) - 0x181F5
1197 - Swamp Maze Control (Panel) - 0x17C0A
1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054
1400 - Caves Mountain Shortcut - 0x2D73F
1400 - Caves Mountain Shortcut (Door) - 0x2D73F
1500 - Symmetry Laser - 0x00509
1501 - Desert Laser - 0x012FB,0x01317
@@ -73,101 +73,101 @@ Doors:
1509 - Swamp Laser - 0x00BF6
1510 - Treehouse Laser - 0x028A4
1600 - Outside Tutorial Optional Door - 0x03BA2
1603 - Outside Tutorial Outpost Entry Door - 0x0A170
1606 - Outside Tutorial Outpost Exit Door - 0x04CA3
1609 - Glass Factory Entry Door - 0x01A29
1612 - Glass Factory Back Wall - 0x0D7ED
1615 - Symmetry Island Lower Door - 0x17F3E
1618 - Symmetry Island Upper Door - 0x18269
1619 - Orchard Middle Gate - 0x03307
1620 - Orchard Final Gate - 0x03313
1621 - Desert Door to Flood Light Room - 0x09FEE
1624 - Desert Door to Pond Room - 0x0C2C3
1627 - Desert Door to Water Levels Room - 0x0A24B
1630 - Desert Door to Elevator Room - 0x0C316
1633 - Quarry Main Entry 1 - 0x09D6F
1636 - Quarry Main Entry 2 - 0x17C07
1639 - Quarry Door to Mill - 0x02010
1642 - Quarry Mill Side Door - 0x275FF
1645 - Quarry Mill Rooftop Shortcut - 0x17CE8
1648 - Quarry Mill Stairs - 0x0368A
1651 - Quarry Boathouse Boat Staircase - 0x2769B,0x27163
1653 - Quarry Boathouse First Barrier - 0x17C50
1654 - Quarry Boathouse Shortcut - 0x3865F
1656 - Shadows Timed Door - 0x19B24
1657 - Shadows Laser Room Right Door - 0x194B2
1660 - Shadows Laser Room Left Door - 0x19665
1663 - Shadows Barrier to Quarry - 0x19865,0x0A2DF
1666 - Shadows Barrier to Ledge - 0x1855B,0x19ADE
1669 - Keep Hedge Maze 1 Exit Door - 0x01954
1672 - Keep Pressure Plates 1 Exit Door - 0x01BEC
1675 - Keep Hedge Maze 2 Shortcut - 0x018CE
1678 - Keep Hedge Maze 2 Exit Door - 0x019D8
1681 - Keep Hedge Maze 3 Shortcut - 0x019B5
1684 - Keep Hedge Maze 3 Exit Door - 0x019E6
1687 - Keep Hedge Maze 4 Shortcut - 0x0199A
1690 - Keep Hedge Maze 4 Exit Door - 0x01A0E
1693 - Keep Pressure Plates 2 Exit Door - 0x01BEA
1696 - Keep Pressure Plates 3 Exit Door - 0x01CD5
1699 - Keep Pressure Plates 4 Exit Door - 0x01D40
1702 - Keep Shortcut to Shadows - 0x09E3D
1705 - Keep Tower Shortcut - 0x04F8F
1708 - Monastery Shortcut - 0x0364E
1711 - Monastery Inner Door - 0x0C128
1714 - Monastery Outer Door - 0x0C153
1717 - Monastery Door to Garden - 0x03750
1718 - Town Cargo Box Door - 0x0A0C9
1720 - Town Wooden Roof Staircase - 0x034F5
1723 - Town Tinted Door to RGB House - 0x28A61
1726 - Town Door to Church - 0x03BB0
1729 - Town Maze Staircase - 0x28AA2
1732 - Town Windmill Door - 0x1845B
1735 - Town RGB House Staircase - 0x2897B
1738 - Town Tower Blue Panels Door - 0x27798
1741 - Town Tower Lattice Door - 0x27799
1744 - Town Tower Environmental Set Door - 0x2779A
1747 - Town Tower Wooden Roof Set Door - 0x2779C
1750 - Theater Entry Door - 0x17F88
1753 - Theater Exit Door Left - 0x0A16D
1756 - Theater Exit Door Right - 0x3CCDF
1759 - Jungle Bamboo Shortcut to River - 0x3873B
1760 - Jungle Popup Wall - 0x1475B
1762 - River Shortcut to Monastery Garden - 0x0CF2A
1765 - Bunker Bunker Entry Door - 0x0C2A4
1768 - Bunker Tinted Glass Door - 0x17C79
1771 - Bunker Door to Ultraviolet Room - 0x0C2A3
1774 - Bunker Door to Elevator - 0x0A08D
1777 - Swamp Entry Door - 0x00C1C
1780 - Swamp Door to Broken Shapers - 0x184B7
1600 - Outside Tutorial Outpost Path (Door) - 0x03BA2
1603 - Outside Tutorial Outpost Entry (Door) - 0x0A170
1606 - Outside Tutorial Outpost Exit (Door) - 0x04CA3
1609 - Glass Factory Entry (Door) - 0x01A29
1612 - Glass Factory Back Wall (Door) - 0x0D7ED
1615 - Symmetry Island Lower (Door) - 0x17F3E
1618 - Symmetry Island Upper (Door) - 0x18269
1619 - Orchard First Gate (Door) - 0x03307
1620 - Orchard Second Gate (Door) - 0x03313
1621 - Desert Light Room Entry (Door) - 0x09FEE
1624 - Desert Pond Room Entry (Door) - 0x0C2C3
1627 - Desert Flood Room Entry (Door) - 0x0A24B
1630 - Desert Elevator Room Entry (Door) - 0x0C316
1633 - Quarry Entry 1 (Door) - 0x09D6F
1636 - Quarry Entry 2 (Door) - 0x17C07
1639 - Quarry Mill Entry (Door) - 0x02010
1642 - Quarry Mill Side Exit (Door) - 0x275FF
1645 - Quarry Mill Roof Exit (Door) - 0x17CE8
1648 - Quarry Mill Stairs (Door) - 0x0368A
1651 - Quarry Boathouse Dock (Door) - 0x2769B,0x27163
1653 - Quarry Boathouse First Barrier (Door) - 0x17C50
1654 - Quarry Boathouse Second Barrier (Door) - 0x3865F
1656 - Shadows Timed Door (Door) - 0x19B24
1657 - Shadows Laser Entry Right (Door) - 0x194B2
1660 - Shadows Laser Entry Left (Door) - 0x19665
1663 - Shadows Quarry Barrier (Door) - 0x19865,0x0A2DF
1666 - Shadows Ledge Barrier (Door) - 0x1855B,0x19ADE
1669 - Keep Hedge Maze 1 Exit (Door) - 0x01954
1672 - Keep Pressure Plates 1 Exit (Door) - 0x01BEC
1675 - Keep Hedge Maze 2 Shortcut (Door) - 0x018CE
1678 - Keep Hedge Maze 2 Exit (Door) - 0x019D8
1681 - Keep Hedge Maze 3 Shortcut (Door) - 0x019B5
1684 - Keep Hedge Maze 3 Exit (Door) - 0x019E6
1687 - Keep Hedge Maze 4 Shortcut (Door) - 0x0199A
1690 - Keep Hedge Maze 4 Exit (Door) - 0x01A0E
1693 - Keep Pressure Plates 2 Exit (Door) - 0x01BEA
1696 - Keep Pressure Plates 3 Exit (Door) - 0x01CD5
1699 - Keep Pressure Plates 4 Exit (Door) - 0x01D40
1702 - Keep Shadows Shortcut (Door) - 0x09E3D
1705 - Keep Tower Shortcut (Door) - 0x04F8F
1708 - Monastery Shortcut (Door) - 0x0364E
1711 - Monastery Entry Inner (Door) - 0x0C128
1714 - Monastery Entry Outer (Door) - 0x0C153
1717 - Monastery Garden Entry (Door) - 0x03750
1718 - Town Cargo Box Entry (Door) - 0x0A0C9
1720 - Town Wooden Roof Stairs (Door) - 0x034F5
1723 - Town Tinted Glass Door (Door) - 0x28A61
1726 - Town Church Entry (Door) - 0x03BB0
1729 - Town Maze Stairs (Door) - 0x28AA2
1732 - Town Windmill Entry (Door) - 0x1845B
1735 - Town RGB House Stairs (Door) - 0x2897B
1738 - Town Tower First Door (Door) - 0x27798
1741 - Town Tower Third Door (Door) - 0x27799
1744 - Town Tower Fourth Door (Door) - 0x2779A
1747 - Town Tower Second Door (Door) - 0x2779C
1750 - Theater Entry (Door) - 0x17F88
1753 - Theater Exit Left (Door) - 0x0A16D
1756 - Theater Exit Right (Door) - 0x3CCDF
1759 - Jungle Bamboo Laser Shortcut (Door) - 0x3873B
1760 - Jungle Popup Wall (Door) - 0x1475B
1762 - River Monastery Shortcut (Door) - 0x0CF2A
1765 - Bunker Entry (Door) - 0x0C2A4
1768 - Bunker Tinted Glass Door (Door) - 0x17C79
1771 - Bunker UV Room Entry (Door) - 0x0C2A3
1774 - Bunker Elevator Room Entry (Door) - 0x0A08D
1777 - Swamp Entry (Door) - 0x00C1C
1780 - Swamp Between Bridges First Door - 0x184B7
1783 - Swamp Platform Shortcut Door - 0x38AE6
1786 - Swamp Cyan Water Pump - 0x04B7F
1789 - Swamp Door to Rotated Shapers - 0x18507
1792 - Swamp Red Water Pump - 0x183F2
1795 - Swamp Red Underwater Exit - 0x305D5
1798 - Swamp Blue Water Pump - 0x18482
1801 - Swamp Purple Water Pump - 0x0A1D6
1804 - Swamp Near Laser Shortcut - 0x2D880
1807 - Treehouse First Door - 0x0C309
1810 - Treehouse Second Door - 0x0C310
1813 - Treehouse Beyond Yellow Bridge Door - 0x0A181
1816 - Treehouse Drawbridge - 0x0C32D
1819 - Treehouse Timed Door to Laser House - 0x0C323
1822 - Inside Mountain First Layer Exit Door - 0x09E54
1825 - Inside Mountain Second Layer Staircase Near - 0x09FFB
1828 - Inside Mountain Second Layer Exit Door - 0x09EDD
1831 - Inside Mountain Second Layer Staircase Far - 0x09E07
1834 - Inside Mountain Giant Puzzle Exit Door - 0x09F89
1840 - Inside Mountain Door to Final Room - 0x0C141
1843 - Inside Mountain Bottom Layer Rock - 0x17F33
1846 - Inside Mountain Door to Secret Area - 0x2D77D
1849 - Caves Pillar Door - 0x019A5
1855 - Caves Swamp Shortcut - 0x2D859
1858 - Challenge Entry Door - 0x0A19A
1861 - Challenge Door to Theater Walkway - 0x0348A
1864 - Theater Walkway Door to Windmill Interior - 0x27739
1867 - Theater Walkway Door to Desert Elevator Room - 0x27263
1870 - Theater Walkway Door to Town - 0x09E87
1786 - Swamp Cyan Water Pump (Door) - 0x04B7F
1789 - Swamp Between Bridges Second Door - 0x18507
1792 - Swamp Red Water Pump (Door) - 0x183F2
1795 - Swamp Red Underwater Exit (Door) - 0x305D5
1798 - Swamp Blue Water Pump (Door) - 0x18482
1801 - Swamp Purple Water Pump (Door) - 0x0A1D6
1804 - Swamp Laser Shortcut (Door) - 0x2D880
1807 - Treehouse First Door (Door) - 0x0C309
1810 - Treehouse Second Door (Door) - 0x0C310
1813 - Treehouse Third Door (Door) - 0x0A181
1816 - Treehouse Drawbridge (Door) - 0x0C32D
1819 - Treehouse Laser House Entry (Door) - 0x0C323
1822 - Mountain Floor 1 Exit (Door) - 0x09E54
1825 - Mountain Floor 2 Staircase Near (Door) - 0x09FFB
1828 - Mountain Floor 2 Exit (Door) - 0x09EDD
1831 - Mountain Floor 2 Staircase Far (Door) - 0x09E07
1834 - Mountain Bottom Floor Giant Puzzle Exit (Door) - 0x09F89
1840 - Mountain Bottom Floor Final Room Entry (Door) - 0x0C141
1843 - Mountain Bottom Floor Rock (Door) - 0x17F33
1846 - Caves Entry (Door) - 0x2D77D
1849 - Caves Pillar Door (Door) - 0x019A5
1855 - Caves Swamp Shortcut (Door) - 0x2D859
1858 - Challenge Entry (Door) - 0x0A19A
1861 - Challenge Tunnels Entry (Door) - 0x0348A
1864 - Tunnels Theater Shortcut (Door) - 0x27739
1867 - Tunnels Desert Shortcut (Door) - 0x27263
1870 - Tunnels Town Shortcut (Door) - 0x09E87
1903 - Outside Tutorial Outpost Doors - 0x03BA2,0x0A170,0x04CA3
1906 - Symmetry Island Doors - 0x17F3E,0x18269
@@ -181,18 +181,18 @@ Doors:
1930 - Keep Hedge Maze Doors - 0x01954,0x018CE,0x019D8,0x019B5,0x019E6,0x0199A,0x01A0E
1933 - Keep Pressure Plates Doors - 0x01BEC,0x01BEA,0x01CD5,0x01D40
1936 - Keep Shortcuts - 0x09E3D,0x04F8F
1939 - Monastery Entry Door - 0x0C128,0x0C153
1939 - Monastery Entry - 0x0C128,0x0C153
1942 - Monastery Shortcuts - 0x0364E,0x03750
1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x1845B,0x2897B
1948 - Town Tower Doors - 0x27798,0x27799,0x2779A,0x2779C
1951 - Theater Exit Door - 0x0A16D,0x3CCDF
1951 - Theater Exit - 0x0A16D,0x3CCDF
1954 - Jungle & River Shortcuts - 0x3873B,0x0CF2A
1957 - Bunker Doors - 0x0C2A4,0x17C79,0x0C2A3,0x0A08D
1960 - Swamp Doors - 0x00C1C,0x184B7,0x38AE6,0x18507
1963 - Swamp Water Pumps - 0x04B7F,0x183F2,0x305D5,0x18482,0x0A1D6
1966 - Treehouse Entry Doors - 0x0C309,0x0C310,0x0A181
1975 - Inside Mountain Second Layer Stairs & Doors - 0x09FFB,0x09EDD,0x09E07
1978 - Inside Mountain Bottom Layer Doors to Caves - 0x17F33,0x2D77D
1975 - Mountain Floor 2 Stairs & Doors - 0x09FFB,0x09EDD,0x09E07
1978 - Mountain Bottom Floor Doors to Caves - 0x17F33,0x2D77D
1981 - Caves Doors to Challenge - 0x019A5,0x0A19A
1984 - Caves Exits to Main Island - 0x2D859,0x2D73F
1987 - Theater Walkway Doors - 0x27739,0x27263,0x09E87
1987 - Tunnels Doors - 0x27739,0x27263,0x09E87

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,7 @@ class WitnessWorld(World):
"""
game = "The Witness"
topology_present = False
data_version = 5
data_version = 7
static_logic = StaticWitnessLogic()
static_locat = StaticWitnessLocations()

View File

@@ -30,17 +30,17 @@ class StaticWitnessLocations:
"Outside Tutorial Vault Box",
"Outside Tutorial Discard",
"Outside Tutorial Dots Introduction 5",
"Outside Tutorial Squares Introduction 9",
"Outside Tutorial Shed Row 5",
"Outside Tutorial Tree Row 9",
"Glass Factory Discard",
"Glass Factory Vertical Symmetry 5",
"Glass Factory Rotational Symmetry 3",
"Glass Factory Back Wall 5",
"Glass Factory Front 3",
"Glass Factory Melting 3",
"Symmetry Island Black Dots 5",
"Symmetry Island Colored Dots 6",
"Symmetry Island Fading Lines 7",
"Symmetry Island Right 5",
"Symmetry Island Back 6",
"Symmetry Island Left 7",
"Symmetry Island Scenery Outlines 5",
"Symmetry Island Laser Panel",
@@ -48,26 +48,28 @@ class StaticWitnessLocations:
"Desert Vault Box",
"Desert Discard",
"Desert Sun Reflection 8",
"Desert Artificial Light Reflection 3",
"Desert Pond Reflection 5",
"Desert Flood Reflection 6",
"Desert Surface 8",
"Desert Light Room 3",
"Desert Pond Room 5",
"Desert Flood Room 6",
"Desert Final Bent 3",
"Desert Final Hexagonal",
"Desert Laser Panel",
"Quarry Mill Eraser and Dots 6",
"Quarry Mill Eraser and Squares 8",
"Quarry Mill Small Squares & Dots & Eraser",
"Quarry Boathouse Intro Shapers",
"Quarry Boathouse Intro Stars",
"Quarry Boathouse Eraser and Shapers 5",
"Quarry Boathouse Stars & Eraser & Shapers 2",
"Quarry Boathouse Stars & Eraser & Shapers 5",
"Quarry Mill Lower Row 6",
"Quarry Mill Upper Row 8",
"Quarry Mill Control Room Right",
"Quarry Boathouse Intro Right",
"Quarry Boathouse Intro Left",
"Quarry Boathouse Front Row 5",
"Quarry Boathouse Back First Row 9",
"Quarry Boathouse Back Second Row 3",
"Quarry Discard",
"Quarry Laser Panel",
"Shadows Lower Avoid 8",
"Shadows Environmental Avoid 8",
"Shadows Follow 5",
"Shadows Intro 8",
"Shadows Far 8",
"Shadows Near 5",
"Shadows Laser Panel",
"Keep Hedge Maze 4",
@@ -79,44 +81,44 @@ class StaticWitnessLocations:
"Shipwreck Vault Box",
"Shipwreck Discard",
"Monastery Rhombic Avoid 3",
"Monastery Branch Follow 2",
"Monastery Outside 3",
"Monastery Inside 4",
"Monastery Laser Panel",
"Town Cargo Box Discard",
"Town Hexagonal Reflection",
"Town Tall Hexagonal",
"Town Church Lattice",
"Town Rooftop Discard",
"Town Symmetry Squares 5 + Dots",
"Town Full Dot Grid Shapers 5",
"Town Shapers & Dots & Eraser",
"Town Red Rooftop 5",
"Town Wooden Roof Lower Row 5",
"Town Wooden Rooftop",
"Town Laser Panel",
"Theater Discard",
"Jungle Discard",
"Jungle Waves 3",
"Jungle Waves 7",
"Jungle First Row 3",
"Jungle Second Row 4",
"Jungle Popup Wall 6",
"Jungle Laser Panel",
"River Vault Box",
"Bunker Drawn Squares 5",
"Bunker Drawn Squares 9",
"Bunker Drawn Squares through Tinted Glass 3",
"Bunker Drop-Down Door Squares 2",
"Bunker Intro Left 5",
"Bunker Intro Back 4",
"Bunker Glass Room 3",
"Bunker UV Room 2",
"Bunker Laser Panel",
"Swamp Seperatable Shapers 6",
"Swamp Combinable Shapers 8",
"Swamp Broken Shapers 4",
"Swamp Cyan Underwater Negative Shapers 5",
"Swamp Platform Shapers 4",
"Swamp Rotated Shapers 4",
"Swamp Red Underwater Negative Shapers 4",
"Swamp More Rotated Shapers 4",
"Swamp Blue Underwater Negative Shapers 5",
"Swamp Intro Front 6",
"Swamp Intro Back 8",
"Swamp Between Bridges Near Row 4",
"Swamp Cyan Underwater 5",
"Swamp Platform Row 4",
"Swamp Between Bridges Far Row 4",
"Swamp Red Underwater 4",
"Swamp Beyond Rotating Bridge 4",
"Swamp Blue Underwater 5",
"Swamp Laser Panel",
"Treehouse Yellow Bridge 9",
@@ -125,73 +127,77 @@ class StaticWitnessLocations:
"Treehouse Green Bridge 7",
"Treehouse Green Bridge Discard",
"Treehouse Left Orange Bridge 15",
"Treehouse Burnt House Discard",
"Treehouse Laser Discard",
"Treehouse Right Orange Bridge 12",
"Treehouse Laser Panel",
"Mountaintop Discard",
"Mountaintop Vault Box",
}
"Mountainside Discard",
"Mountainside Vault Box",
UNCOMMON_LOCATIONS = {
"Mountaintop River Shape",
"Tutorial Patio Floor",
"Quarry Mill Big Squares & Dots & Eraser",
"Quarry Mill Control Room Left",
"Theater Tutorial Video",
"Theater Desert Video",
"Theater Jungle Video",
"Theater Shipwreck Video",
"Theater Mountain Video",
"Town RGB Squares",
"Town RGB Stars",
"Swamp Underwater Back Optional",
"Town RGB Room Left",
"Town RGB Room Right",
"Swamp Purple Underwater",
}
CAVES_LOCATIONS = {
"Inside Mountain Caves Dot Grid Triangles 4",
"Inside Mountain Caves Symmetry Triangles",
"Inside Mountain Caves Stars & Squares and Triangles 2",
"Inside Mountain Caves Shapers and Triangles 2",
"Inside Mountain Caves Symmetry Shapers",
"Inside Mountain Caves Broken and Negative Shapers",
"Inside Mountain Caves Broken Shapers",
"Caves Blue Tunnel Right First 4",
"Caves Blue Tunnel Left First 1",
"Caves Blue Tunnel Left Second 5",
"Caves Blue Tunnel Right Second 5",
"Caves Blue Tunnel Right Third 1",
"Caves Blue Tunnel Left Fourth 1",
"Caves Blue Tunnel Left Third 1",
"Inside Mountain Caves Rainbow Squares",
"Inside Mountain Caves Squares & Stars and Colored Eraser",
"Inside Mountain Caves Rotated Broken Shapers",
"Inside Mountain Caves Stars and Squares",
"Inside Mountain Caves Lone Pillar",
"Inside Mountain Caves Wooden Beam Shapers",
"Inside Mountain Caves Wooden Beam Squares and Shapers",
"Inside Mountain Caves Wooden Beam Stars and Squares",
"Inside Mountain Caves Wooden Beam Shapers and Stars",
"Inside Mountain Caves Upstairs Invisible Dots 8",
"Inside Mountain Caves Upstairs Invisible Dot Symmetry 3",
"Inside Mountain Caves Upstairs Dot Grid Negative Shapers",
"Inside Mountain Caves Upstairs Dot Grid Rotated Shapers",
"Caves First Floor Middle",
"Caves First Floor Right",
"Caves First Floor Left",
"Caves First Floor Grounded",
"Caves Lone Pillar",
"Caves First Wooden Beam",
"Caves Second Wooden Beam",
"Caves Third Wooden Beam",
"Caves Fourth Wooden Beam",
"Caves Right Upstairs Left Row 8",
"Caves Right Upstairs Right Row 3",
"Caves Left Upstairs Single",
"Caves Left Upstairs Left Row 5",
"Theater Walkway Vault Box",
"Inside Mountain Bottom Layer Discard",
"Tunnels Vault Box",
"Mountain Bottom Floor Discard",
"Theater Challenge Video",
}
MOUNTAIN_UNREACHABLE_FROM_BEHIND = {
"Mountaintop Trap Door Triple Exit",
"Inside Mountain Obscured Vision 5",
"Inside Mountain Moving Background 7",
"Inside Mountain Physically Obstructed 3",
"Inside Mountain Angled Inside Trash 2",
"Inside Mountain Color Cycle 5",
"Inside Mountain Same Solution 6",
"Mountain Floor 1 Right Row 5",
"Mountain Floor 1 Left Row 7",
"Mountain Floor 1 Back Row 3",
"Mountain Floor 1 Trash Pillar 2",
"Mountain Floor 2 Near Row 5",
"Mountain Floor 2 Far Row 6",
}
MOUNTAIN_REACHABLE_FROM_BEHIND = {
"Inside Mountain Elevator Discard",
"Inside Mountain Giant Puzzle",
"Mountain Floor 2 Elevator Discard",
"Mountain Bottom Floor Giant Puzzle",
"Inside Mountain Final Room Left Pillar 4",
"Inside Mountain Final Room Right Pillar 4",
"Mountain Final Room Left Pillar 4",
"Mountain Final Room Right Pillar 4",
}
MOUNTAIN_EXTRAS = {
"Challenge Vault Box",
"Theater Challenge Video",
"Mountain Bottom Floor Discard"
}
ALL_LOCATIONS_TO_ID = dict()
@@ -241,37 +247,44 @@ class WitnessPlayerLocations:
StaticWitnessLocations.GENERAL_LOCATIONS
)
doors = get_option_value(world, player, "shuffle_doors")
doors = get_option_value(world, player, "shuffle_doors") >= 2
earlyutm = is_option_enabled(world, player, "early_secret_area")
victory = get_option_value(world, player, "victory_condition")
lasers = get_option_value(world, player, "challenge_lasers")
mount_lasers = get_option_value(world, player, "mountain_lasers")
chal_lasers = get_option_value(world, player, "challenge_lasers")
laser_shuffle = get_option_value(world, player, "shuffle_lasers")
postgame = set()
postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS
postgame = postgame | StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND
postgame = postgame | StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND
postgame = postgame | StaticWitnessLocations.MOUNTAIN_EXTRAS
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | postgame
if earlyutm or doors >= 2 or (victory == 1 and (lasers <= 11 or laser_shuffle)):
mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mount_lasers)
if earlyutm or doors: # in non-doors, there is no way to get symbol-locked by the final pillars (currently)
postgame -= StaticWitnessLocations.CAVES_LOCATIONS
if doors >= 2:
if (doors or earlyutm) and (victory == 0 or (victory == 2 and mount_lasers > chal_lasers)):
postgame -= {"Challenge Vault Box", "Theater Challenge Video"}
if doors or mountain_enterable_from_top:
postgame -= StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND
if victory != 2:
if mountain_enterable_from_top:
postgame -= StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND
if (victory == 0 and doors) or victory == 1 or (victory == 2 and mount_lasers > chal_lasers and doors):
postgame -= {"Mountain Bottom Floor Discard"}
if is_option_enabled(world, player, "shuffle_discarded_panels"):
self.PANEL_TYPES_TO_SHUFFLE.add("Discard")
if is_option_enabled(world, player, "shuffle_vault_boxes"):
self.PANEL_TYPES_TO_SHUFFLE.add("Vault")
if is_option_enabled(world, player, "shuffle_uncommon"):
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.UNCOMMON_LOCATIONS
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS
if not is_option_enabled(world, player, "shuffle_postgame"):

View File

@@ -60,7 +60,10 @@ class WitnessPlayerLogic:
for dependentItem in door_items:
all_options.add(items_option.union(dependentItem))
return frozenset(all_options)
if panel_hex != "0x28A0D":
return frozenset(all_options)
else: # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved
these_items = all_options
these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"]
@@ -321,7 +324,7 @@ class WitnessPlayerLogic:
self.VICTORY_LOCATION = "0x0356B"
self.EVENT_ITEM_NAMES = {
"0x01A0F": "Keep Laser Panel (Hedge Mazes) Activates",
"0x09D9B": "Monastery Overhead Doors Open",
"0x09D9B": "Monastery Shutters Open",
"0x193A6": "Monastery Laser Panel Activates",
"0x00037": "Monastery Branch Panels Activate",
"0x0A079": "Access to Bunker Laser",
@@ -332,24 +335,24 @@ class WitnessPlayerLogic:
"0x01D3F": "Keep Laser Panel (Pressure Plates) Activates",
"0x09F7F": "Mountain Access",
"0x0367C": "Quarry Laser Mill Requirement Met",
"0x009A1": "Swamp Rotated Shapers 1 Activates",
"0x009A1": "Swamp Between Bridges Far 1 Activates",
"0x00006": "Swamp Cyan Water Drains",
"0x00990": "Swamp Broken Shapers 1 Activates",
"0x0A8DC": "Lower Avoid 6 Activates",
"0x0000A": "Swamp More Rotated Shapers 1 Access",
"0x09E86": "Inside Mountain Second Layer Blue Bridge Access",
"0x09ED8": "Inside Mountain Second Layer Yellow Bridge Access",
"0x00990": "Swamp Between Bridges Near Row 1 Activates",
"0x0A8DC": "Intro 6 Activates",
"0x0000A": "Swamp Beyond Rotating Bridge 1 Access",
"0x09E86": "Mountain Floor 2 Blue Bridge Access",
"0x09ED8": "Mountain Floor 2 Yellow Bridge Access",
"0x0A3D0": "Quarry Laser Boathouse Requirement Met",
"0x00596": "Swamp Red Water Drains",
"0x00E3A": "Swamp Purple Water Drains",
"0x0343A": "Door to Symmetry Island Powers On",
"0xFFF00": "Inside Mountain Bottom Layer Discard Turns On",
"0xFFF00": "Mountain Bottom Floor Discard Turns On",
"0x17CA6": "All Boat Panels Turn On",
"0x17CDF": "All Boat Panels Turn On",
"0x09DB8": "All Boat Panels Turn On",
"0x17C95": "All Boat Panels Turn On",
"0x03BB0": "Town Church Lattice Vision From Outside",
"0x28AC1": "Town Shapers & Dots & Eraser Turns On",
"0x28AC1": "Town Wooden Rooftop Turns On",
"0x28A69": "Town Tower 1st Door Opens",
"0x28ACC": "Town Tower 2nd Door Opens",
"0x28AD9": "Town Tower 3rd Door Opens",
@@ -357,9 +360,9 @@ class WitnessPlayerLogic:
"0x03675": "Quarry Mill Ramp Activation From Above",
"0x03679": "Quarry Mill Lift Lowering While Standing On It",
"0x2FAF6": "Tutorial Gate Secret Solution Knowledge",
"0x079DF": "Town Hexagonal Reflection Turns On",
"0x079DF": "Town Tall Hexagonal Turns On",
"0x17DA2": "Right Orange Bridge Fully Extended",
"0x19B24": "Shadows Lower Avoid Patterns Visible",
"0x19B24": "Shadows Intro Patterns Visible",
"0x2700B": "Open Door to Treehouse Laser House",
"0x00055": "Orchard Apple Trees 4 Turns On",
"0x17DDB": "Left Orange Bridge Fully Extended",
@@ -369,6 +372,8 @@ class WitnessPlayerLogic:
"0x03481": "Tutorial Video Pattern Knowledge",
"0x03702": "Jungle Video Pattern Knowledge",
"0x0356B": "Challenge Video Pattern Knowledge",
"0x0A15F": "Desert Laser Panel Shutters Open (1)",
"0x012D7": "Desert Laser Panel Shutters Open (2)",
}
self.ALWAYS_EVENT_NAMES_BY_HEX = {

View File

@@ -73,7 +73,7 @@ class WitnessRegions:
all_locations = all_locations | set(locations_for_this_region)
world.regions += [
create_region(world, player, region_name, self.locat,locations_for_this_region)
create_region(world, player, region_name, self.locat, locations_for_this_region)
]
for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items():

View File

@@ -25,84 +25,84 @@ Disabled Locations:
0x00055 (Orchard Apple Tree 3)
0x032F7 (Orchard Apple Tree 4)
0x032FF (Orchard Apple Tree 5)
0x198B5 (Shadows Lower Avoid 1)
0x198BD (Shadows Lower Avoid 2)
0x198BF (Shadows Lower Avoid 3)
0x19771 (Shadows Lower Avoid 4)
0x0A8DC (Shadows Lower Avoid 5)
0x0AC74 (Shadows Lower Avoid 6)
0x0AC7A (Shadows Lower Avoid 7)
0x0A8E0 (Shadows Lower Avoid 8)
0x386FA (Shadows Environmental Avoid 1)
0x1C33F (Shadows Environmental Avoid 2)
0x196E2 (Shadows Environmental Avoid 3)
0x1972A (Shadows Environmental Avoid 4)
0x19809 (Shadows Environmental Avoid 5)
0x19806 (Shadows Environmental Avoid 6)
0x196F8 (Shadows Environmental Avoid 7)
0x1972F (Shadows Environmental Avoid 8)
0x19797 (Shadows Follow 1)
0x1979A (Shadows Follow 2)
0x197E0 (Shadows Follow 3)
0x197E8 (Shadows Follow 4)
0x197E5 (Shadows Follow 5)
0x198B5 (Shadows Intro 1)
0x198BD (Shadows Intro 2)
0x198BF (Shadows Intro 3)
0x19771 (Shadows Intro 4)
0x0A8DC (Shadows Intro 5)
0x0AC74 (Shadows Intro 6)
0x0AC7A (Shadows Intro 7)
0x0A8E0 (Shadows Intro 8)
0x386FA (Shadows Far 1)
0x1C33F (Shadows Far 2)
0x196E2 (Shadows Far 3)
0x1972A (Shadows Far 4)
0x19809 (Shadows Far 5)
0x19806 (Shadows Far 6)
0x196F8 (Shadows Far 7)
0x1972F (Shadows Far 8)
0x19797 (Shadows Near 1)
0x1979A (Shadows Near 2)
0x197E0 (Shadows Near 3)
0x197E8 (Shadows Near 4)
0x197E5 (Shadows Near 5)
0x19650 (Shadows Laser)
0x00139 (Keep Hedge Maze 1)
0x019DC (Keep Hedge Maze 2)
0x019E7 (Keep Hedge Maze 3)
0x01A0F (Keep Hedge Maze 4)
0x0360E (Laser Hedges)
0x00B10 (Monastery Door Open Left)
0x00C92 (Monastery Door Open Right)
0x00290 (Monastery Rhombic Avoid 1)
0x00038 (Monastery Rhombic Avoid 2)
0x00037 (Monastery Rhombic Avoid 3)
0x193A7 (Monastery Branch Avoid 1)
0x193AA (Monastery Branch Avoid 2)
0x193AB (Monastery Branch Follow 1)
0x193A6 (Monastery Branch Follow 2)
0x00B10 (Monastery Entry Left)
0x00C92 (Monastery Entry Right)
0x00290 (Monastery Outside 1)
0x00038 (Monastery Outside 2)
0x00037 (Monastery Outside 3)
0x193A7 (Monastery Inside 1)
0x193AA (Monastery Inside 2)
0x193AB (Monastery Inside 3)
0x193A6 (Monastery Inside 4)
0x17CA4 (Monastery Laser)
0x18590 (Tree Outlines) - True - Symmetry & Environment
0x28AE3 (Vines Shadows Follow) - 0x18590 - Shadows Follow & Environment
0x28938 (Four-way Apple Tree) - 0x28AE3 - Environment
0x079DF (Triple Environmental Puzzle) - 0x28938 - Shadows Avoid & Environment & Reflection
0x28B39 (Hexagonal Reflection) - 0x079DF & 0x2896A - Reflection
0x18590 (Transparent) - True - Symmetry & Environment
0x28AE3 (Vines) - 0x18590 - Shadows Follow & Environment
0x28938 (Apple Tree) - 0x28AE3 - Environment
0x079DF (Triple Exit) - 0x28938 - Shadows Avoid & Environment & Reflection
0x28B39 (Tall Hexagonal) - 0x079DF & 0x2896A - Reflection
0x03553 (Theater Tutorial Video)
0x03552 (Theater Desert Video)
0x0354E (Theater Jungle Video)
0x03549 (Theater Challenge Video)
0x0354F (Theater Shipwreck Video)
0x03545 (Theater Mountain Video)
0x002C4 (Waves 1)
0x00767 (Waves 2)
0x002C6 (Waves 3)
0x0070E (Waves 4)
0x0070F (Waves 5)
0x0087D (Waves 6)
0x002C7 (Waves 7)
0x15ADD (River Rhombic Avoid Vault)
0x002C4 (First Row 1)
0x00767 (First Row 2)
0x002C6 (First Row 3)
0x0070E (Second Row 1)
0x0070F (Second Row 2)
0x0087D (Second Row 3)
0x002C7 (Second Row 4)
0x15ADD (River Outside Vault)
0x03702 (River Vault Box)
0x17CAA (Rhombic Avoid to Monastery Garden)
0x17CAA (Monastery Shortcut Panel)
0x17C2E (Door to Bunker)
0x09F7D (Bunker Drawn Squares 1)
0x09FDC (Bunker Drawn Squares 2)
0x09FF7 (Bunker Drawn Squares 3)
0x09F82 (Bunker Drawn Squares 4)
0x09FF8 (Bunker Drawn Squares 5)
0x09D9F (Bunker Drawn Squares 6)
0x09DA1 (Bunker Drawn Squares 7)
0x09DA2 (Bunker Drawn Squares 8)
0x09DAF (Bunker Drawn Squares 9)
0x0A010 (Bunker Drawn Squares through Tinted Glass 1)
0x0A01B (Bunker Drawn Squares through Tinted Glass 2)
0x0A01F (Bunker Drawn Squares through Tinted Glass 3)
0x0A099 (Door to Bunker Proper)
0x09F7D (Bunker Intro Left 1)
0x09FDC (Bunker Intro Left 2)
0x09FF7 (Bunker Intro Left 3)
0x09F82 (Bunker Intro Left 4)
0x09FF8 (Bunker Intro Left 5)
0x09D9F (Bunker Intro Back 1)
0x09DA1 (Bunker Intro Back 2)
0x09DA2 (Bunker Intro Back 3)
0x09DAF (Bunker Intro Back 4)
0x0A010 (Bunker Glass Room 1)
0x0A01B (Bunker Glass Room 2)
0x0A01F (Bunker Glass Room 3)
0x0A099 (Tinted Glass Door)
0x34BC5 (Bunker Drop-Down Door Open)
0x34BC6 (Bunker Drop-Down Door Close)
0x17E63 (Bunker Drop-Down Door Squares 1)
0x17E67 (Bunker Drop-Down Door Squares 2)
0x17E63 (Bunker UV Room 1)
0x17E67 (Bunker UV Room 2)
0x09DE0 (Bunker Laser)
0x0A079 (Bunker Elevator Control)
0x0042D (Mountaintop River Shape)
0x17CAA (River Door to Garden Panel)
0x17CAA (River Garden Entry Panel)

View File

@@ -1,31 +1,31 @@
Items:
Glass Factory Entry Door (Panel)
Door to Symmetry Island Lower (Panel)
Door to Symmetry Island Upper (Panel)
Door to Desert Flood Light Room (Panel)
Desert Flood Room Flood Controls (Panel)
Quarry Door to Mill (Panel)
Glass Factory Entry (Panel)
Symmetry Island Lower (Panel)
Symmetry Island Upper (Panel)
Desert Light Room Entry (Panel)
Desert Flood Controls (Panel)
Quarry Mill Entry (Panel)
Quarry Mill Ramp Controls (Panel)
Quarry Mill Elevator Controls (Panel)
Quarry Mill Lift Controls (Panel)
Quarry Boathouse Ramp Height Control (Panel)
Quarry Boathouse Ramp Horizontal Control (Panel)
Shadows Door Timer (Panel)
Monastery Entry Door Left (Panel)
Monastery Entry Door Right (Panel)
Town Door to RGB House (Panel)
Town Door to Church (Panel)
Monastery Entry Left (Panel)
Monastery Entry Right (Panel)
Town Tinted Glass Door (Panel)
Town Church Entry (Panel)
Town Maze Panel (Drop-Down Staircase) (Panel)
Windmill Door (Panel)
Windmill Entry (Panel)
Treehouse First & Second Doors (Panel)
Treehouse Third Door (Panel)
Treehouse Laser House Door Timer (Panel)
Treehouse Shortcut Drop-Down Bridge (Panel)
Treehouse Drawbridge (Panel)
Jungle Popup Wall (Panel)
Bunker Entry Door (Panel)
Inside Bunker Door to Bunker Proper (Panel)
Bunker Entry (Panel)
Bunker Tinted Glass Door (Panel)
Bunker Elevator Control (Panel)
Swamp Entry Door (Panel)
Swamp Entry (Panel)
Swamp Sliding Bridge (Panel)
Swamp Rotating Bridge (Panel)
Swamp Maze Control (Panel)
Boat
Boat

View File

@@ -1,128 +1,128 @@
Items:
Outside Tutorial Optional Door
Outside Tutorial Outpost Entry Door
Outside Tutorial Outpost Exit Door
Glass Factory Entry Door
Glass Factory Back Wall
Symmetry Island Lower Door
Symmetry Island Upper Door
Orchard Middle Gate
Orchard Final Gate
Desert Door to Flood Light Room
Desert Door to Pond Room
Desert Door to Water Levels Room
Desert Door to Elevator Room
Quarry Main Entry 1
Quarry Main Entry 2
Quarry Door to Mill
Quarry Mill Side Door
Quarry Mill Rooftop Shortcut
Quarry Mill Stairs
Quarry Boathouse Boat Staircase
Quarry Boathouse First Barrier
Quarry Boathouse Shortcut
Shadows Timed Door
Shadows Laser Room Right Door
Shadows Laser Room Left Door
Shadows Barrier to Quarry
Shadows Barrier to Ledge
Keep Hedge Maze 1 Exit Door
Keep Pressure Plates 1 Exit Door
Keep Hedge Maze 2 Shortcut
Keep Hedge Maze 2 Exit Door
Keep Hedge Maze 3 Shortcut
Keep Hedge Maze 3 Exit Door
Keep Hedge Maze 4 Shortcut
Keep Hedge Maze 4 Exit Door
Keep Pressure Plates 2 Exit Door
Keep Pressure Plates 3 Exit Door
Keep Pressure Plates 4 Exit Door
Keep Shortcut to Shadows
Keep Tower Shortcut
Monastery Shortcut
Monastery Inner Door
Monastery Outer Door
Monastery Door to Garden
Town Cargo Box Door
Town Wooden Roof Staircase
Town Tinted Door to RGB House
Town Door to Church
Town Maze Staircase
Town Windmill Door
Town RGB House Staircase
Town Tower Blue Panels Door
Town Tower Lattice Door
Town Tower Environmental Set Door
Town Tower Wooden Roof Set Door
Theater Entry Door
Theater Exit Door Left
Theater Exit Door Right
Jungle Bamboo Shortcut to River
Jungle Popup Wall
River Shortcut to Monastery Garden
Bunker Bunker Entry Door
Bunker Tinted Glass Door
Bunker Door to Ultraviolet Room
Bunker Door to Elevator
Swamp Entry Door
Swamp Door to Broken Shapers
Outside Tutorial Outpost Path (Door)
Outside Tutorial Outpost Entry (Door)
Outside Tutorial Outpost Exit (Door)
Glass Factory Entry (Door)
Glass Factory Back Wall (Door)
Symmetry Island Lower (Door)
Symmetry Island Upper (Door)
Orchard First Gate (Door)
Orchard Second Gate (Door)
Desert Light Room Entry (Door)
Desert Pond Room Entry (Door)
Desert Flood Room Entry (Door)
Desert Elevator Room Entry (Door)
Quarry Entry 1 (Door)
Quarry Entry 2 (Door)
Quarry Mill Entry (Door)
Quarry Mill Side Exit (Door)
Quarry Mill Roof Exit (Door)
Quarry Mill Stairs (Door)
Quarry Boathouse Dock (Door)
Quarry Boathouse First Barrier (Door)
Quarry Boathouse Second Barrier (Door)
Shadows Timed Door (Door)
Shadows Laser Entry Right (Door)
Shadows Laser Entry Left (Door)
Shadows Quarry Barrier (Door)
Shadows Ledge Barrier (Door)
Keep Hedge Maze 1 Exit (Door)
Keep Pressure Plates 1 Exit (Door)
Keep Hedge Maze 2 Shortcut (Door)
Keep Hedge Maze 2 Exit (Door)
Keep Hedge Maze 3 Shortcut (Door)
Keep Hedge Maze 3 Exit (Door)
Keep Hedge Maze 4 Shortcut (Door)
Keep Hedge Maze 4 Exit (Door)
Keep Pressure Plates 2 Exit (Door)
Keep Pressure Plates 3 Exit (Door)
Keep Pressure Plates 4 Exit (Door)
Keep Shadows Shortcut (Door)
Keep Tower Shortcut (Door)
Monastery Shortcut (Door)
Monastery Entry Inner (Door)
Monastery Entry Outer (Door)
Monastery Garden Entry (Door)
Town Cargo Box Entry (Door)
Town Wooden Roof Stairs (Door)
Town Tinted Glass Door (Door)
Town Church Entry (Door)
Town Maze Stairs (Door)
Town Windmill Entry (Door)
Town RGB House Stairs (Door)
Town Tower First Door (Door)
Town Tower Third Door (Door)
Town Tower Fourth Door (Door)
Town Tower Second Door (Door)
Theater Entry (Door)
Theater Exit Left (Door)
Theater Exit Right (Door)
Jungle Bamboo Laser Shortcut (Door)
Jungle Popup Wall (Door)
River Monastery Shortcut (Door)
Bunker Entry (Door)
Bunker Tinted Glass Door (Door)
Bunker UV Room Entry (Door)
Bunker Elevator Room Entry (Door)
Swamp Entry (Door)
Swamp Between Bridges First Door
Swamp Platform Shortcut Door
Swamp Cyan Water Pump
Swamp Door to Rotated Shapers
Swamp Red Water Pump
Swamp Red Underwater Exit
Swamp Blue Water Pump
Swamp Purple Water Pump
Swamp Near Laser Shortcut
Treehouse First Door
Treehouse Second Door
Treehouse Beyond Yellow Bridge Door
Treehouse Drawbridge
Treehouse Timed Door to Laser House
Inside Mountain First Layer Exit Door
Inside Mountain Second Layer Staircase Near
Inside Mountain Second Layer Exit Door
Inside Mountain Second Layer Staircase Far
Inside Mountain Giant Puzzle Exit Door
Inside Mountain Door to Final Room
Inside Mountain Bottom Layer Rock
Inside Mountain Door to Secret Area
Caves Pillar Door
Caves Mountain Shortcut
Caves Swamp Shortcut
Challenge Entry Door
Challenge Door to Theater Walkway
Theater Walkway Door to Windmill Interior
Theater Walkway Door to Desert Elevator Room
Theater Walkway Door to Town
Swamp Cyan Water Pump (Door)
Swamp Between Bridges Second Door
Swamp Red Water Pump (Door)
Swamp Red Underwater Exit (Door)
Swamp Blue Water Pump (Door)
Swamp Purple Water Pump (Door)
Swamp Laser Shortcut (Door)
Treehouse First Door (Door)
Treehouse Second Door (Door)
Treehouse Third Door (Door)
Treehouse Drawbridge (Door)
Treehouse Laser House Entry (Door)
Mountain Floor 1 Exit (Door)
Mountain Floor 2 Staircase Near (Door)
Mountain Floor 2 Exit (Door)
Mountain Floor 2 Staircase Far (Door)
Mountain Bottom Floor Giant Puzzle Exit (Door)
Mountain Bottom Floor Final Room Entry (Door)
Mountain Bottom Floor Rock (Door)
Caves Entry (Door)
Caves Pillar Door (Door)
Caves Mountain Shortcut (Door)
Caves Swamp Shortcut (Door)
Challenge Entry (Door)
Challenge Tunnels Entry (Door)
Tunnels Theater Shortcut (Door)
Tunnels Desert Shortcut (Door)
Tunnels Town Shortcut (Door)
Added Locations:
Outside Tutorial Door to Outpost Panel
Outside Tutorial Exit Door from Outpost Panel
Glass Factory Entry Door Panel
Glass Factory Vertical Symmetry 5
Symmetry Island Door to Symmetry Island Lower Panel
Symmetry Island Door to Symmetry Island Upper Panel
Outside Tutorial Outpost Entry Panel
Outside Tutorial Outpost Exit Panel
Glass Factory Entry Panel
Glass Factory Back Wall 5
Symmetry Island Lower Panel
Symmetry Island Upper Panel
Orchard Apple Tree 3
Orchard Apple Tree 5
Desert Door to Desert Flood Light Room Panel
Desert Artificial Light Reflection 3
Desert Door to Water Levels Room Panel
Desert Flood Reflection 6
Quarry Door to Quarry 1 Panel
Quarry Door to Quarry 2 Panel
Quarry Door to Mill Right
Quarry Door to Mill Left
Quarry Mill Ground Floor Shortcut Door Panel
Quarry Mill Door to Outside Quarry Stairs Panel
Desert Light Room Entry Panel
Desert Light Room 3
Desert Flood Room Entry Panel
Desert Flood Room 6
Quarry Entry 1 Panel
Quarry Entry 2 Panel
Quarry Mill Entry Right Panel
Quarry Mill Entry Left Panel
Quarry Mill Side Exit Panel
Quarry Mill Roof Exit Panel
Quarry Mill Stair Control
Quarry Boathouse Shortcut Door Panel
Quarry Boathouse Second Barrier Panel
Shadows Door Timer Inside
Shadows Door Timer Outside
Shadows Environmental Avoid 8
Shadows Follow 5
Shadows Lower Avoid 3
Shadows Lower Avoid 5
Shadows Far 8
Shadows Near 5
Shadows Intro 3
Shadows Intro 5
Keep Hedge Maze 1
Keep Pressure Plates 1
Keep Hedge Maze 2
@@ -131,71 +131,70 @@ Keep Hedge Maze 4
Keep Pressure Plates 2
Keep Pressure Plates 3
Keep Pressure Plates 4
Keep Shortcut to Shadows Panel
Keep Tower Shortcut to Keep Panel
Monastery Shortcut Door Panel
Monastery Door Open Left
Monastery Door Open Right
Monastery Rhombic Avoid 3
Town Cargo Box Panel
Town Full Dot Grid Shapers 5
Town Tinted Door Panel
Town Door to Church Stars Panel
Keep Shadows Shortcut Panel
Keep Tower Shortcut Panel
Monastery Shortcut Panel
Monastery Entry Left
Monastery Entry Right
Monastery Outside 3
Town Cargo Box Entry Panel
Town Wooden Roof Lower Row 5
Town Tinted Glass Door Panel
Town Church Entry Panel
Town Maze Stair Control
Town Windmill Door Panel
Town Sound Room Left
Town Windmill Entry Panel
Town Sound Room Right
Town Symmetry Squares 5 + Dots
Town Red Rooftop 5
Town Church Lattice
Town Hexagonal Reflection
Town Shapers & Dots & Eraser
Windmill Door to Front of Theater Panel
Theater Door to Cargo Box Left Panel
Theater Door to Cargo Box Right Panel
Jungle Shortcut to River Panel
Town Tall Hexagonal
Town Wooden Rooftop
Windmill Theater Entry Panel
Theater Exit Left Panel
Theater Exit Right Panel
Jungle Laser Shortcut Panel
Jungle Popup Wall Control
River Rhombic Avoid to Monastery Garden
Bunker Bunker Entry Panel
Bunker Door to Bunker Proper Panel
Bunker Drawn Squares through Tinted Glass 3
Bunker Drop-Down Door Squares 2
River Monastery Shortcut Panel
Bunker Entry Panel
Bunker Tinted Glass Door Panel
Bunker Glass Room 3
Bunker UV Room 2
Swamp Entry Panel
Swamp Platform Shapers 4
Swamp Platform Row 4
Swamp Platform Shortcut Right Panel
Swamp Blue Underwater Negative Shapers 5
Swamp Broken Shapers 4
Swamp Cyan Underwater Negative Shapers 5
Swamp Red Underwater Negative Shapers 4
Swamp More Rotated Shapers 4
Swamp More Rotated Shapers 4
Swamp Near Laser Shortcut Right Panel
Swamp Blue Underwater 5
Swamp Between Bridges Near Row 4
Swamp Cyan Underwater 5
Swamp Red Underwater 4
Swamp Beyond Rotating Bridge 4
Swamp Beyond Rotating Bridge 4
Swamp Laser Shortcut Right Panel
Treehouse First Door Panel
Treehouse Second Door Panel
Treehouse Beyond Yellow Bridge Door Panel
Treehouse Third Door Panel
Treehouse Bridge Control
Treehouse Left Orange Bridge 15
Treehouse Right Orange Bridge 12
Treehouse Laser House Door Timer Outside Control
Treehouse Laser House Door Timer Inside Control
Inside Mountain Moving Background 7
Inside Mountain Obscured Vision 5
Inside Mountain Physically Obstructed 3
Inside Mountain Angled Inside Trash 2
Inside Mountain Color Cycle 5
Inside Mountain Light Bridge Controller 2
Inside Mountain Light Bridge Controller 3
Inside Mountain Same Solution 6
Inside Mountain Giant Puzzle
Inside Mountain Door to Final Room Left
Inside Mountain Door to Final Room Right
Inside Mountain Bottom Layer Discard
Inside Mountain Rock Control
Inside Mountain Secret Area Entry Panel
Inside Mountain Caves Lone Pillar
Inside Mountain Caves Shortcut to Mountain Panel
Inside Mountain Caves Shortcut to Swamp Panel
Inside Mountain Caves Challenge Entry Panel
Challenge Door to Theater Walkway Panel
Theater Walkway Theater Shortcut Panel
Theater Walkway Desert Shortcut Panel
Theater Walkway Town Shortcut Panel
Treehouse Laser House Door Timer Inside
Mountain Floor 1 Left Row 7
Mountain Floor 1 Right Row 5
Mountain Floor 1 Back Row 3
Mountain Floor 1 Trash Pillar 2
Mountain Floor 2 Near Row 5
Mountain Floor 2 Light Bridge Controller Near
Mountain Floor 2 Light Bridge Controller Far
Mountain Floor 2 Far Row 6
Mountain Bottom Floor Giant Puzzle
Mountain Bottom Floor Final Room Entry Left
Mountain Bottom Floor Final Room Entry Right
Mountain Bottom Floor Discard
Mountain Bottom Floor Rock Control
Mountain Bottom Floor Caves Entry Panel
Caves Lone Pillar
Caves Mountain Shortcut Panel
Caves Swamp Shortcut Panel
Caves Challenge Entry Panel
Challenge Tunnels Entry Panel
Tunnels Theater Shortcut Panel
Tunnels Desert Shortcut Panel
Tunnels Town Shortcut Panel

View File

@@ -1,104 +1,104 @@
Items:
Outside Tutorial Optional Door
Outside Tutorial Outpost Entry Door
Outside Tutorial Outpost Exit Door
Glass Factory Entry Door
Glass Factory Back Wall
Symmetry Island Lower Door
Symmetry Island Upper Door
Orchard Middle Gate
Orchard Final Gate
Desert Door to Flood Light Room
Desert Door to Pond Room
Desert Door to Water Levels Room
Desert Door to Elevator Room
Quarry Main Entry 1
Quarry Main Entry 2
Quarry Door to Mill
Quarry Mill Side Door
Quarry Mill Rooftop Shortcut
Quarry Mill Stairs
Quarry Boathouse Boat Staircase
Quarry Boathouse First Barrier
Quarry Boathouse Shortcut
Shadows Timed Door
Shadows Laser Room Right Door
Shadows Laser Room Left Door
Shadows Barrier to Quarry
Shadows Barrier to Ledge
Keep Hedge Maze 1 Exit Door
Keep Pressure Plates 1 Exit Door
Keep Hedge Maze 2 Shortcut
Keep Hedge Maze 2 Exit Door
Keep Hedge Maze 3 Shortcut
Keep Hedge Maze 3 Exit Door
Keep Hedge Maze 4 Shortcut
Keep Hedge Maze 4 Exit Door
Keep Pressure Plates 2 Exit Door
Keep Pressure Plates 3 Exit Door
Keep Pressure Plates 4 Exit Door
Keep Shortcut to Shadows
Keep Tower Shortcut
Monastery Shortcut
Monastery Inner Door
Monastery Outer Door
Monastery Door to Garden
Town Cargo Box Door
Town Wooden Roof Staircase
Town Tinted Door to RGB House
Town Door to Church
Town Maze Staircase
Town Windmill Door
Town RGB House Staircase
Town Tower Blue Panels Door
Town Tower Lattice Door
Town Tower Environmental Set Door
Town Tower Wooden Roof Set Door
Theater Entry Door
Theater Exit Door Left
Theater Exit Door Right
Jungle Bamboo Shortcut to River
Jungle Popup Wall
River Shortcut to Monastery Garden
Bunker Bunker Entry Door
Bunker Tinted Glass Door
Bunker Door to Ultraviolet Room
Bunker Door to Elevator
Swamp Entry Door
Swamp Door to Broken Shapers
Outside Tutorial Outpost Path (Door)
Outside Tutorial Outpost Entry (Door)
Outside Tutorial Outpost Exit (Door)
Glass Factory Entry (Door)
Glass Factory Back Wall (Door)
Symmetry Island Lower (Door)
Symmetry Island Upper (Door)
Orchard First Gate (Door)
Orchard Second Gate (Door)
Desert Light Room Entry (Door)
Desert Pond Room Entry (Door)
Desert Flood Room Entry (Door)
Desert Elevator Room Entry (Door)
Quarry Entry 1 (Door)
Quarry Entry 2 (Door)
Quarry Mill Entry (Door)
Quarry Mill Side Exit (Door)
Quarry Mill Roof Exit (Door)
Quarry Mill Stairs (Door)
Quarry Boathouse Dock (Door)
Quarry Boathouse First Barrier (Door)
Quarry Boathouse Second Barrier (Door)
Shadows Timed Door (Door)
Shadows Laser Entry Right (Door)
Shadows Laser Entry Left (Door)
Shadows Quarry Barrier (Door)
Shadows Ledge Barrier (Door)
Keep Hedge Maze 1 Exit (Door)
Keep Pressure Plates 1 Exit (Door)
Keep Hedge Maze 2 Shortcut (Door)
Keep Hedge Maze 2 Exit (Door)
Keep Hedge Maze 3 Shortcut (Door)
Keep Hedge Maze 3 Exit (Door)
Keep Hedge Maze 4 Shortcut (Door)
Keep Hedge Maze 4 Exit (Door)
Keep Pressure Plates 2 Exit (Door)
Keep Pressure Plates 3 Exit (Door)
Keep Pressure Plates 4 Exit (Door)
Keep Shadows Shortcut (Door)
Keep Tower Shortcut (Door)
Monastery Shortcut (Door)
Monastery Entry Inner (Door)
Monastery Entry Outer (Door)
Monastery Garden Entry (Door)
Town Cargo Box Entry (Door)
Town Wooden Roof Stairs (Door)
Town Tinted Glass Door (Door)
Town Church Entry (Door)
Town Maze Stairs (Door)
Town Windmill Entry (Door)
Town RGB House Stairs (Door)
Town Tower First Door (Door)
Town Tower Third Door (Door)
Town Tower Fourth Door (Door)
Town Tower Second Door (Door)
Theater Entry (Door)
Theater Exit Left (Door)
Theater Exit Right (Door)
Jungle Bamboo Laser Shortcut (Door)
Jungle Popup Wall (Door)
River Monastery Shortcut (Door)
Bunker Entry (Door)
Bunker Tinted Glass Door (Door)
Bunker UV Room Entry (Door)
Bunker Elevator Room Entry (Door)
Swamp Entry (Door)
Swamp Between Bridges First Door
Swamp Platform Shortcut Door
Swamp Cyan Water Pump
Swamp Door to Rotated Shapers
Swamp Red Water Pump
Swamp Red Underwater Exit
Swamp Blue Water Pump
Swamp Purple Water Pump
Swamp Near Laser Shortcut
Treehouse First Door
Treehouse Second Door
Treehouse Beyond Yellow Bridge Door
Treehouse Drawbridge
Treehouse Timed Door to Laser House
Inside Mountain First Layer Exit Door
Inside Mountain Second Layer Staircase Near
Inside Mountain Second Layer Exit Door
Inside Mountain Second Layer Staircase Far
Inside Mountain Giant Puzzle Exit Door
Inside Mountain Door to Final Room
Inside Mountain Bottom Layer Rock
Inside Mountain Door to Secret Area
Caves Pillar Door
Caves Mountain Shortcut
Caves Swamp Shortcut
Challenge Entry Door
Challenge Door to Theater Walkway
Theater Walkway Door to Windmill Interior
Theater Walkway Door to Desert Elevator Room
Theater Walkway Door to Town
Swamp Cyan Water Pump (Door)
Swamp Between Bridges Second Door
Swamp Red Water Pump (Door)
Swamp Red Underwater Exit (Door)
Swamp Blue Water Pump (Door)
Swamp Purple Water Pump (Door)
Swamp Laser Shortcut (Door)
Treehouse First Door (Door)
Treehouse Second Door (Door)
Treehouse Third Door (Door)
Treehouse Drawbridge (Door)
Treehouse Laser House Entry (Door)
Mountain Floor 1 Exit (Door)
Mountain Floor 2 Staircase Near (Door)
Mountain Floor 2 Exit (Door)
Mountain Floor 2 Staircase Far (Door)
Mountain Bottom Floor Giant Puzzle Exit (Door)
Mountain Bottom Floor Final Room Entry (Door)
Mountain Bottom Floor Rock (Door)
Caves Entry (Door)
Caves Pillar Door (Door)
Caves Mountain Shortcut (Door)
Caves Swamp Shortcut (Door)
Challenge Entry (Door)
Challenge Tunnels Entry (Door)
Tunnels Theater Shortcut (Door)
Tunnels Desert Shortcut (Door)
Tunnels Town Shortcut (Door)
Desert Flood Room Flood Controls (Panel)
Desert Flood Controls (Panel)
Quarry Mill Ramp Controls (Panel)
Quarry Mill Elevator Controls (Panel)
Quarry Mill Lift Controls (Panel)
Quarry Boathouse Ramp Height Control (Panel)
Quarry Boathouse Ramp Horizontal Control (Panel)
Bunker Elevator Control (Panel)
@@ -108,32 +108,32 @@ Swamp Maze Control (Panel)
Boat
Added Locations:
Outside Tutorial Door to Outpost Panel
Outside Tutorial Exit Door from Outpost Panel
Glass Factory Entry Door Panel
Glass Factory Vertical Symmetry 5
Symmetry Island Door to Symmetry Island Lower Panel
Symmetry Island Door to Symmetry Island Upper Panel
Outside Tutorial Outpost Entry Panel
Outside Tutorial Outpost Exit Panel
Glass Factory Entry Panel
Glass Factory Back Wall 5
Symmetry Island Lower Panel
Symmetry Island Upper Panel
Orchard Apple Tree 3
Orchard Apple Tree 5
Desert Door to Desert Flood Light Room Panel
Desert Artificial Light Reflection 3
Desert Door to Water Levels Room Panel
Desert Flood Reflection 6
Quarry Door to Quarry 1 Panel
Quarry Door to Quarry 2 Panel
Quarry Door to Mill Right
Quarry Door to Mill Left
Quarry Mill Ground Floor Shortcut Door Panel
Quarry Mill Door to Outside Quarry Stairs Panel
Desert Light Room Entry Panel
Desert Light Room 3
Desert Flood Room Entry Panel
Desert Flood Room 6
Quarry Entry 1 Panel
Quarry Entry 2 Panel
Quarry Mill Entry Right Panel
Quarry Mill Entry Left Panel
Quarry Mill Side Exit Panel
Quarry Mill Roof Exit Panel
Quarry Mill Stair Control
Quarry Boathouse Shortcut Door Panel
Quarry Boathouse Second Barrier Panel
Shadows Door Timer Inside
Shadows Door Timer Outside
Shadows Environmental Avoid 8
Shadows Follow 5
Shadows Lower Avoid 3
Shadows Lower Avoid 5
Shadows Far 8
Shadows Near 5
Shadows Intro 3
Shadows Intro 5
Keep Hedge Maze 1
Keep Pressure Plates 1
Keep Hedge Maze 2
@@ -142,71 +142,70 @@ Keep Hedge Maze 4
Keep Pressure Plates 2
Keep Pressure Plates 3
Keep Pressure Plates 4
Keep Shortcut to Shadows Panel
Keep Tower Shortcut to Keep Panel
Monastery Shortcut Door Panel
Monastery Door Open Left
Monastery Door Open Right
Monastery Rhombic Avoid 3
Town Cargo Box Panel
Town Full Dot Grid Shapers 5
Town Tinted Door Panel
Town Door to Church Stars Panel
Keep Shadows Shortcut Panel
Keep Tower Shortcut Panel
Monastery Shortcut Panel
Monastery Entry Left
Monastery Entry Right
Monastery Outside 3
Town Cargo Box Entry Panel
Town Wooden Roof Lower Row 5
Town Tinted Glass Door Panel
Town Church Entry Panel
Town Maze Stair Control
Town Windmill Door Panel
Town Sound Room Left
Town Windmill Entry Panel
Town Sound Room Right
Town Symmetry Squares 5 + Dots
Town Red Rooftop 5
Town Church Lattice
Town Hexagonal Reflection
Town Shapers & Dots & Eraser
Windmill Door to Front of Theater Panel
Theater Door to Cargo Box Left Panel
Theater Door to Cargo Box Right Panel
Jungle Shortcut to River Panel
Town Tall Hexagonal
Town Wooden Rooftop
Windmill Theater Entry Panel
Theater Exit Left Panel
Theater Exit Right Panel
Jungle Laser Shortcut Panel
Jungle Popup Wall Control
River Rhombic Avoid to Monastery Garden
Bunker Bunker Entry Panel
Bunker Door to Bunker Proper Panel
Bunker Drawn Squares through Tinted Glass 3
Bunker Drop-Down Door Squares 2
River Monastery Shortcut Panel
Bunker Entry Panel
Bunker Tinted Glass Door Panel
Bunker Glass Room 3
Bunker UV Room 2
Swamp Entry Panel
Swamp Platform Shapers 4
Swamp Platform Row 4
Swamp Platform Shortcut Right Panel
Swamp Blue Underwater Negative Shapers 5
Swamp Broken Shapers 4
Swamp Cyan Underwater Negative Shapers 5
Swamp Red Underwater Negative Shapers 4
Swamp More Rotated Shapers 4
Swamp More Rotated Shapers 4
Swamp Near Laser Shortcut Right Panel
Swamp Blue Underwater 5
Swamp Between Bridges Near Row 4
Swamp Cyan Underwater 5
Swamp Red Underwater 4
Swamp Beyond Rotating Bridge 4
Swamp Beyond Rotating Bridge 4
Swamp Laser Shortcut Right Panel
Treehouse First Door Panel
Treehouse Second Door Panel
Treehouse Beyond Yellow Bridge Door Panel
Treehouse Third Door Panel
Treehouse Bridge Control
Treehouse Left Orange Bridge 15
Treehouse Right Orange Bridge 12
Treehouse Laser House Door Timer Outside Control
Treehouse Laser House Door Timer Inside Control
Inside Mountain Moving Background 7
Inside Mountain Obscured Vision 5
Inside Mountain Physically Obstructed 3
Inside Mountain Angled Inside Trash 2
Inside Mountain Color Cycle 5
Inside Mountain Light Bridge Controller 2
Inside Mountain Light Bridge Controller 3
Inside Mountain Same Solution 6
Inside Mountain Giant Puzzle
Inside Mountain Door to Final Room Left
Inside Mountain Door to Final Room Right
Inside Mountain Bottom Layer Discard
Inside Mountain Rock Control
Inside Mountain Secret Area Entry Panel
Inside Mountain Caves Lone Pillar
Inside Mountain Caves Shortcut to Mountain Panel
Inside Mountain Caves Shortcut to Swamp Panel
Inside Mountain Caves Challenge Entry Panel
Challenge Door to Theater Walkway Panel
Theater Walkway Theater Shortcut Panel
Theater Walkway Desert Shortcut Panel
Theater Walkway Town Shortcut Panel
Treehouse Laser House Door Timer Inside
Mountain Floor 1 Left Row 7
Mountain Floor 1 Right Row 5
Mountain Floor 1 Back Row 3
Mountain Floor 1 Trash Pillar 2
Mountain Floor 2 Near Row 5
Mountain Floor 2 Light Bridge Controller Near
Mountain Floor 2 Light Bridge Controller Far
Mountain Floor 2 Far Row 6
Mountain Bottom Floor Giant Puzzle
Mountain Bottom Floor Final Room Entry Left
Mountain Bottom Floor Final Room Entry Right
Mountain Bottom Floor Discard
Mountain Bottom Floor Rock Control
Mountain Bottom Floor Caves Entry Panel
Caves Lone Pillar
Caves Mountain Shortcut Panel
Caves Swamp Shortcut Panel
Caves Challenge Entry Panel
Challenge Tunnels Entry Panel
Tunnels Theater Shortcut Panel
Tunnels Desert Shortcut Panel
Tunnels Town Shortcut Panel

View File

@@ -1,73 +1,73 @@
Items:
Glass Factory Back Wall
Quarry Boathouse Boat Staircase
Glass Factory Back Wall (Door)
Quarry Boathouse Dock (Door)
Outside Tutorial Outpost Doors
Glass Factory Entry Door
Glass Factory Entry (Door)
Symmetry Island Doors
Orchard Gates
Desert Doors
Quarry Main Entry
Quarry Door to Mill
Quarry Mill Entry (Door)
Quarry Mill Shortcuts
Quarry Boathouse Barriers
Shadows Timed Door
Shadows Timed Door (Door)
Shadows Laser Room Door
Shadows Barriers
Keep Hedge Maze Doors
Keep Pressure Plates Doors
Keep Shortcuts
Monastery Entry Door
Monastery Entry
Monastery Shortcuts
Town Doors
Town Tower Doors
Theater Entry Door
Theater Exit Door
Theater Entry (Door)
Theater Exit
Jungle & River Shortcuts
Jungle Popup Wall
Jungle Popup Wall (Door)
Bunker Doors
Swamp Doors
Swamp Near Laser Shortcut
Swamp Laser Shortcut (Door)
Swamp Water Pumps
Treehouse Entry Doors
Treehouse Drawbridge
Treehouse Timed Door to Laser House
Inside Mountain First Layer Exit Door
Inside Mountain Second Layer Stairs & Doors
Inside Mountain Giant Puzzle Exit Door
Inside Mountain Door to Final Room
Inside Mountain Bottom Layer Doors to Caves
Treehouse Drawbridge (Door)
Treehouse Laser House Entry (Door)
Mountain Floor 1 Exit (Door)
Mountain Floor 2 Stairs & Doors
Mountain Bottom Floor Giant Puzzle Exit (Door)
Mountain Bottom Floor Final Room Entry (Door)
Mountain Bottom Floor Doors to Caves
Caves Doors to Challenge
Caves Exits to Main Island
Challenge Door to Theater Walkway
Theater Walkway Doors
Challenge Tunnels Entry (Door)
Tunnels Doors
Added Locations:
Outside Tutorial Door to Outpost Panel
Outside Tutorial Exit Door from Outpost Panel
Glass Factory Entry Door Panel
Glass Factory Vertical Symmetry 5
Symmetry Island Door to Symmetry Island Lower Panel
Symmetry Island Door to Symmetry Island Upper Panel
Outside Tutorial Outpost Entry Panel
Outside Tutorial Outpost Exit Panel
Glass Factory Entry Panel
Glass Factory Back Wall 5
Symmetry Island Lower Panel
Symmetry Island Upper Panel
Orchard Apple Tree 3
Orchard Apple Tree 5
Desert Door to Desert Flood Light Room Panel
Desert Artificial Light Reflection 3
Desert Door to Water Levels Room Panel
Desert Flood Reflection 6
Quarry Door to Quarry 1 Panel
Quarry Door to Quarry 2 Panel
Quarry Door to Mill Right
Quarry Door to Mill Left
Quarry Mill Ground Floor Shortcut Door Panel
Quarry Mill Door to Outside Quarry Stairs Panel
Desert Light Room Entry Panel
Desert Light Room 3
Desert Flood Room Entry Panel
Desert Flood Room 6
Quarry Entry 1 Panel
Quarry Entry 2 Panel
Quarry Mill Entry Right Panel
Quarry Mill Entry Left Panel
Quarry Mill Side Exit Panel
Quarry Mill Roof Exit Panel
Quarry Mill Stair Control
Quarry Boathouse Shortcut Door Panel
Quarry Boathouse Second Barrier Panel
Shadows Door Timer Inside
Shadows Door Timer Outside
Shadows Environmental Avoid 8
Shadows Follow 5
Shadows Lower Avoid 3
Shadows Lower Avoid 5
Shadows Far 8
Shadows Near 5
Shadows Intro 3
Shadows Intro 5
Keep Hedge Maze 1
Keep Pressure Plates 1
Keep Hedge Maze 2
@@ -76,71 +76,70 @@ Keep Hedge Maze 4
Keep Pressure Plates 2
Keep Pressure Plates 3
Keep Pressure Plates 4
Keep Shortcut to Shadows Panel
Keep Tower Shortcut to Keep Panel
Monastery Shortcut Door Panel
Monastery Door Open Left
Monastery Door Open Right
Monastery Rhombic Avoid 3
Town Cargo Box Panel
Town Full Dot Grid Shapers 5
Town Tinted Door Panel
Town Door to Church Stars Panel
Keep Shadows Shortcut Panel
Keep Tower Shortcut Panel
Monastery Shortcut Panel
Monastery Entry Left
Monastery Entry Right
Monastery Outside 3
Town Cargo Box Entry Panel
Town Wooden Roof Lower Row 5
Town Tinted Glass Door Panel
Town Church Entry Panel
Town Maze Stair Control
Town Windmill Door Panel
Town Sound Room Left
Town Windmill Entry Panel
Town Sound Room Right
Town Symmetry Squares 5 + Dots
Town Red Rooftop 5
Town Church Lattice
Town Hexagonal Reflection
Town Shapers & Dots & Eraser
Windmill Door to Front of Theater Panel
Theater Door to Cargo Box Left Panel
Theater Door to Cargo Box Right Panel
Jungle Shortcut to River Panel
Town Tall Hexagonal
Town Wooden Rooftop
Windmill Theater Entry Panel
Theater Exit Left Panel
Theater Exit Right Panel
Jungle Laser Shortcut Panel
Jungle Popup Wall Control
River Rhombic Avoid to Monastery Garden
Bunker Bunker Entry Panel
Bunker Door to Bunker Proper Panel
Bunker Drawn Squares through Tinted Glass 3
Bunker Drop-Down Door Squares 2
River Monastery Shortcut Panel
Bunker Entry Panel
Bunker Tinted Glass Door Panel
Bunker Glass Room 3
Bunker UV Room 2
Swamp Entry Panel
Swamp Platform Shapers 4
Swamp Platform Row 4
Swamp Platform Shortcut Right Panel
Swamp Blue Underwater Negative Shapers 5
Swamp Broken Shapers 4
Swamp Cyan Underwater Negative Shapers 5
Swamp Red Underwater Negative Shapers 4
Swamp More Rotated Shapers 4
Swamp More Rotated Shapers 4
Swamp Near Laser Shortcut Right Panel
Swamp Blue Underwater 5
Swamp Between Bridges Near Row 4
Swamp Cyan Underwater 5
Swamp Red Underwater 4
Swamp Beyond Rotating Bridge 4
Swamp Beyond Rotating Bridge 4
Swamp Laser Shortcut Right Panel
Treehouse First Door Panel
Treehouse Second Door Panel
Treehouse Beyond Yellow Bridge Door Panel
Treehouse Third Door Panel
Treehouse Bridge Control
Treehouse Left Orange Bridge 15
Treehouse Right Orange Bridge 12
Treehouse Laser House Door Timer Outside Control
Treehouse Laser House Door Timer Inside Control
Inside Mountain Moving Background 7
Inside Mountain Obscured Vision 5
Inside Mountain Physically Obstructed 3
Inside Mountain Angled Inside Trash 2
Inside Mountain Color Cycle 5
Inside Mountain Light Bridge Controller 2
Inside Mountain Light Bridge Controller 3
Inside Mountain Same Solution 6
Inside Mountain Giant Puzzle
Inside Mountain Door to Final Room Left
Inside Mountain Door to Final Room Right
Inside Mountain Bottom Layer Discard
Inside Mountain Rock Control
Inside Mountain Secret Area Entry Panel
Inside Mountain Caves Lone Pillar
Inside Mountain Caves Shortcut to Mountain Panel
Inside Mountain Caves Shortcut to Swamp Panel
Inside Mountain Caves Challenge Entry Panel
Challenge Door to Theater Walkway Panel
Theater Walkway Theater Shortcut Panel
Theater Walkway Desert Shortcut Panel
Theater Walkway Town Shortcut Panel
Treehouse Laser House Door Timer Inside
Mountain Floor 1 Left Row 7
Mountain Floor 1 Right Row 5
Mountain Floor 1 Back Row 3
Mountain Floor 1 Trash Pillar 2
Mountain Floor 2 Near Row 5
Mountain Floor 2 Light Bridge Controller Near
Mountain Floor 2 Light Bridge Controller Far
Mountain Floor 2 Far Row 6
Mountain Bottom Floor Giant Puzzle
Mountain Bottom Floor Final Room Entry Left
Mountain Bottom Floor Final Room Entry Right
Mountain Bottom Floor Discard
Mountain Bottom Floor Rock Control
Mountain Bottom Floor Caves Entry Panel
Caves Lone Pillar
Caves Mountain Shortcut Panel
Caves Swamp Shortcut Panel
Caves Challenge Entry Panel
Challenge Tunnels Entry Panel
Tunnels Theater Shortcut Panel
Tunnels Desert Shortcut Panel
Tunnels Town Shortcut Panel

View File

@@ -5,5 +5,5 @@ Starting Inventory:
Caves Exits to Main Island
Remove Items:
Caves Mountain Shortcut
Caves Swamp Shortcut
Caves Mountain Shortcut (Door)
Caves Swamp Shortcut (Door)