Compare commits

..

29 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
60 changed files with 2482 additions and 1799 deletions

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

@@ -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:
@@ -900,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:
@@ -1335,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.")
@@ -1355,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},
@@ -1859,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)
@@ -1890,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:
@@ -2044,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

@@ -295,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
@@ -790,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")
@@ -807,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)

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

@@ -103,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.
@@ -121,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
@@ -188,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
@@ -209,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
@@ -274,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
```
@@ -323,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):
@@ -352,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
@@ -553,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:
@@ -603,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.
@@ -622,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):
@@ -633,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

@@ -17,7 +17,7 @@ from Launcher import components, icon_paths
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
import subprocess
import pkg_resources
requirement = 'cx-Freeze==6.10'
requirement = 'cx-Freeze>=6.11'
try:
pkg_resources.require(requirement)
import cx_Freeze
@@ -70,7 +70,7 @@ def _threaded_hash(filepath):
# cx_Freeze's build command runs other commands. Override to accept --yes and store that.
class BuildCommand(cx_Freeze.dist.build):
class BuildCommand(cx_Freeze.command.build.Build):
user_options = [
('yes', 'y', 'Answer "yes" to all questions.'),
]
@@ -87,8 +87,8 @@ class BuildCommand(cx_Freeze.dist.build):
# Override cx_Freeze's build_exe command for pre and post build steps
class BuildExeCommand(cx_Freeze.dist.build_exe):
user_options = cx_Freeze.dist.build_exe.user_options + [
class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [
('yes', 'y', 'Answer "yes" to all questions.'),
('extra-data=', None, 'Additional files to add.'),
]

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

@@ -221,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."""
@@ -242,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

View File

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

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

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

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