Compare commits
35 Commits
use_sphinx
...
factorio_d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63fb888191 | ||
|
|
38eef5ac00 | ||
|
|
3e627f80fd | ||
|
|
0d6aeea9fd | ||
|
|
6cd1e8a295 | ||
|
|
1cbe5ae669 | ||
|
|
c5b5ad495c | ||
|
|
813ee5ee3b | ||
|
|
be1158ad78 | ||
|
|
5b3f4460b8 | ||
|
|
de8eff39b3 | ||
|
|
6d5ddf3cad | ||
|
|
809bda02d1 | ||
|
|
2d5ec6ce22 | ||
|
|
a95d0ce9ef | ||
|
|
267d9234e5 | ||
|
|
4686881566 | ||
|
|
101dab0ea4 | ||
|
|
c2d69cb05e | ||
|
|
58f66e0f42 | ||
|
|
0215e1fa28 | ||
|
|
1c0a93acad | ||
|
|
4fcde135e5 | ||
|
|
332dde154f | ||
|
|
8d51205e8f | ||
|
|
ff05e9d7d5 | ||
|
|
516a52c041 | ||
|
|
9daa64741b | ||
|
|
af11fa5150 | ||
|
|
156e9e0e43 | ||
|
|
ef46979bd8 | ||
|
|
b2aa251c47 | ||
|
|
e204a0fce6 | ||
|
|
bb386d3bd7 | ||
|
|
88a225764a |
1
.gitignore
vendored
@@ -21,7 +21,6 @@
|
||||
*.archipelago
|
||||
*.apsave
|
||||
|
||||
docs/sphinx/_build/
|
||||
build
|
||||
bundle/components.wxs
|
||||
dist
|
||||
|
||||
@@ -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' %
|
||||
|
||||
@@ -5,6 +5,7 @@ import urllib.parse
|
||||
import sys
|
||||
import typing
|
||||
import time
|
||||
import functools
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
@@ -17,7 +18,8 @@ if __name__ == "__main__":
|
||||
Utils.init_logging("TextClient", exception_logger="Client")
|
||||
|
||||
from MultiServer import CommandProcessor
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
|
||||
ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
|
||||
from Utils import Version, stream_input
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
import os
|
||||
@@ -204,6 +206,10 @@ class CommonContext:
|
||||
# execution
|
||||
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
|
||||
|
||||
@functools.cached_property
|
||||
def raw_text_parser(self) -> RawJSONtoTextParser:
|
||||
return RawJSONtoTextParser(self)
|
||||
|
||||
@property
|
||||
def total_locations(self) -> typing.Optional[int]:
|
||||
"""Will return None until connected."""
|
||||
|
||||
53
FF1Client.py
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import time
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
@@ -6,7 +7,7 @@ from typing import List
|
||||
|
||||
|
||||
import Utils
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
||||
get_base_parser
|
||||
|
||||
SYSTEM_MESSAGE_ID = 0
|
||||
@@ -64,7 +65,7 @@ class FF1Context(CommonContext):
|
||||
|
||||
def _set_message(self, msg: str, msg_id: int):
|
||||
if DISPLAY_MSGS:
|
||||
self.messages[(time.time(), msg_id)] = msg
|
||||
self.messages[time.time(), msg_id] = msg
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == 'Connected':
|
||||
@@ -73,32 +74,28 @@ class FF1Context(CommonContext):
|
||||
msg = args['text']
|
||||
if ': !' not in msg:
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "ReceivedItems":
|
||||
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == 'PrintJSON':
|
||||
print_type = args['type']
|
||||
item = args['item']
|
||||
receiving_player_id = args['receiving']
|
||||
receiving_player_name = self.player_names[receiving_player_id]
|
||||
sending_player_id = item.player
|
||||
sending_player_name = self.player_names[item.player]
|
||||
if print_type == 'Hint':
|
||||
msg = f"Hint: Your {self.item_names[item.item]} is at" \
|
||||
f" {self.player_names[item.player]}'s {self.location_names[item.location]}"
|
||||
self._set_message(msg, item.item)
|
||||
elif print_type == 'ItemSend' and receiving_player_id != self.slot:
|
||||
if sending_player_id == self.slot:
|
||||
if receiving_player_id == self.slot:
|
||||
msg = f"You found your own {self.item_names[item.item]}"
|
||||
else:
|
||||
msg = f"You sent {self.item_names[item.item]} to {receiving_player_name}"
|
||||
else:
|
||||
if receiving_player_id == sending_player_id:
|
||||
msg = f"{sending_player_name} found their {self.item_names[item.item]}"
|
||||
else:
|
||||
msg = f"{sending_player_name} sent {self.item_names[item.item]} to " \
|
||||
f"{receiving_player_name}"
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
if self.ui:
|
||||
self.ui.print_json(copy.deepcopy(args["data"]))
|
||||
else:
|
||||
text = self.jsontotextparser(copy.deepcopy(args["data"]))
|
||||
logger.info(text)
|
||||
relevant = args.get("type", None) in {"Hint", "ItemSend"}
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
# goes to this world
|
||||
if self.slot_concerns_self(args["receiving"]):
|
||||
relevant = True
|
||||
# found in this world
|
||||
elif self.slot_concerns_self(item.player):
|
||||
relevant = True
|
||||
# not related
|
||||
else:
|
||||
relevant = False
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
|
||||
self._set_message(msg, item.item)
|
||||
|
||||
def run_gui(self):
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
62
Generate.py
@@ -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
@@ -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()
|
||||
|
||||
|
||||
208
MultiServer.py
@@ -126,6 +126,7 @@ class Context:
|
||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||
forced_auto_forfeits: typing.Dict[str, bool]
|
||||
non_hintable_names: typing.Dict[str, typing.Set[str]]
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
|
||||
@@ -196,7 +197,7 @@ class Context:
|
||||
self.item_name_groups = {}
|
||||
self.all_item_and_group_names = {}
|
||||
self.forced_auto_forfeits = collections.defaultdict(lambda: False)
|
||||
self.non_hintable_names = {}
|
||||
self.non_hintable_names = collections.defaultdict(frozenset)
|
||||
|
||||
self._load_game_data()
|
||||
self._init_game_data()
|
||||
@@ -221,11 +222,11 @@ class Context:
|
||||
self.all_item_and_group_names[game_name] = \
|
||||
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
|
||||
|
||||
def item_names_for_game(self, game: str) -> typing.Dict[str, int]:
|
||||
return self.gamespackage[game]["item_name_to_id"]
|
||||
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
||||
return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None
|
||||
|
||||
def location_names_for_game(self, game: str) -> typing.Dict[str, int]:
|
||||
return self.gamespackage[game]["location_name_to_id"]
|
||||
def location_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
||||
return self.gamespackage[game]["location_name_to_id"] if game in self.gamespackage else None
|
||||
|
||||
# General networking
|
||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||
@@ -743,6 +744,7 @@ async def countdown(ctx: Context, timer: int):
|
||||
broadcast_countdown(ctx, 0, f"[Server]: GO")
|
||||
ctx.countdown_timer = 0
|
||||
|
||||
|
||||
def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}):
|
||||
old_clients, new_clients = [], []
|
||||
|
||||
@@ -755,8 +757,10 @@ def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {})
|
||||
ctx.broadcast(old_clients, [{"cmd": "Print", "text": text }])
|
||||
ctx.broadcast(new_clients, [{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
|
||||
|
||||
|
||||
def broadcast_countdown(ctx: Context, timer: int, message: str):
|
||||
broadcast_text_all(ctx, message, { "type": "Countdown", "countdown": timer })
|
||||
broadcast_text_all(ctx, message, {"type": "Countdown", "countdown": timer})
|
||||
|
||||
|
||||
def get_players_string(ctx: Context):
|
||||
auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth}
|
||||
@@ -897,14 +901,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
ctx.save()
|
||||
|
||||
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]:
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]:
|
||||
hints = []
|
||||
slots: typing.Set[int] = {slot}
|
||||
for group_id, group in ctx.groups.items():
|
||||
if slot in group:
|
||||
slots.add(group_id)
|
||||
|
||||
seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name]
|
||||
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
|
||||
for finding_player, check_data in ctx.locations.items():
|
||||
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
|
||||
if receiving_player in slots and item_id == seeked_item_id:
|
||||
@@ -1332,13 +1336,33 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
|
||||
f"You have {points_available} points.")
|
||||
return True
|
||||
|
||||
elif input_text.isnumeric():
|
||||
game = self.ctx.games[self.client.slot]
|
||||
hint_id = int(input_text)
|
||||
hint_name = self.ctx.item_names[hint_id] \
|
||||
if not for_location and hint_id in self.ctx.item_names \
|
||||
else self.ctx.location_names[hint_id] \
|
||||
if for_location and hint_id in self.ctx.location_names \
|
||||
else None
|
||||
if hint_name in self.ctx.non_hintable_names[game]:
|
||||
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||
hints = []
|
||||
elif not for_location:
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id)
|
||||
else:
|
||||
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id)
|
||||
|
||||
else:
|
||||
game = self.ctx.games[self.client.slot]
|
||||
if game not in self.ctx.all_item_and_group_names:
|
||||
self.output("Can't look up item/location for unknown game. Hint for ID instead.")
|
||||
return False
|
||||
names = self.ctx.location_names_for_game(game) \
|
||||
if for_location else \
|
||||
self.ctx.all_item_and_group_names[game]
|
||||
hint_name, usable, response = get_intended_text(input_text,
|
||||
names)
|
||||
hint_name, usable, response = get_intended_text(input_text, names)
|
||||
|
||||
if usable:
|
||||
if hint_name in self.ctx.non_hintable_names[game]:
|
||||
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||
@@ -1352,63 +1376,65 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
else: # location name
|
||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
cost = self.ctx.get_hint_cost(self.client.slot)
|
||||
if hints:
|
||||
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
||||
old_hints = set(hints) - new_hints
|
||||
if old_hints:
|
||||
notify_hints(self.ctx, self.client.team, list(old_hints))
|
||||
if not new_hints:
|
||||
self.output("Hint was previously used, no points deducted.")
|
||||
if new_hints:
|
||||
found_hints = [hint for hint in new_hints if hint.found]
|
||||
not_found_hints = [hint for hint in new_hints if not hint.found]
|
||||
|
||||
if not not_found_hints: # everything's been found, no need to pay
|
||||
can_pay = 1000
|
||||
elif cost:
|
||||
can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call
|
||||
else:
|
||||
can_pay = 1000
|
||||
|
||||
self.ctx.random.shuffle(not_found_hints)
|
||||
# By popular vote, make hints prefer non-local placements
|
||||
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
|
||||
|
||||
hints = found_hints
|
||||
while can_pay > 0:
|
||||
if not not_found_hints:
|
||||
break
|
||||
hint = not_found_hints.pop()
|
||||
hints.append(hint)
|
||||
can_pay -= 1
|
||||
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
|
||||
if not_found_hints:
|
||||
if hints and cost and int((points_available // cost) == 0):
|
||||
self.output(
|
||||
f"There may be more hintables, however, you cannot afford to pay for any more. "
|
||||
f" You have {points_available} and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
elif hints:
|
||||
self.output(
|
||||
"There may be more hintables, you can rerun the command to find more.")
|
||||
else:
|
||||
self.output(f"You can't afford the hint. "
|
||||
f"You have {points_available} points and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
notify_hints(self.ctx, self.client.team, hints)
|
||||
self.ctx.save()
|
||||
return True
|
||||
|
||||
else:
|
||||
self.output("Nothing found. Item/Location may not exist.")
|
||||
return False
|
||||
else:
|
||||
self.output(response)
|
||||
return False
|
||||
|
||||
if hints:
|
||||
cost = self.ctx.get_hint_cost(self.client.slot)
|
||||
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
||||
old_hints = set(hints) - new_hints
|
||||
if old_hints:
|
||||
notify_hints(self.ctx, self.client.team, list(old_hints))
|
||||
if not new_hints:
|
||||
self.output("Hint was previously used, no points deducted.")
|
||||
if new_hints:
|
||||
found_hints = [hint for hint in new_hints if hint.found]
|
||||
not_found_hints = [hint for hint in new_hints if not hint.found]
|
||||
|
||||
if not not_found_hints: # everything's been found, no need to pay
|
||||
can_pay = 1000
|
||||
elif cost:
|
||||
can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call
|
||||
else:
|
||||
can_pay = 1000
|
||||
|
||||
self.ctx.random.shuffle(not_found_hints)
|
||||
# By popular vote, make hints prefer non-local placements
|
||||
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
|
||||
|
||||
hints = found_hints
|
||||
while can_pay > 0:
|
||||
if not not_found_hints:
|
||||
break
|
||||
hint = not_found_hints.pop()
|
||||
hints.append(hint)
|
||||
can_pay -= 1
|
||||
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
|
||||
if not_found_hints:
|
||||
if hints and cost and int((points_available // cost) == 0):
|
||||
self.output(
|
||||
f"There may be more hintables, however, you cannot afford to pay for any more. "
|
||||
f" You have {points_available} and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
elif hints:
|
||||
self.output(
|
||||
"There may be more hintables, you can rerun the command to find more.")
|
||||
else:
|
||||
self.output(f"You can't afford the hint. "
|
||||
f"You have {points_available} points and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
notify_hints(self.ctx, self.client.team, hints)
|
||||
self.ctx.save()
|
||||
return True
|
||||
|
||||
else:
|
||||
self.output("Nothing found. Item/Location may not exist.")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_hint(self, item_name: str = "") -> bool:
|
||||
"""Use !hint {item_name},
|
||||
@@ -1856,17 +1882,25 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||
item_name = " ".join(item_name)
|
||||
game = self.ctx.games[slot]
|
||||
item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game])
|
||||
full_name = " ".join(item_name)
|
||||
|
||||
if full_name.isnumeric():
|
||||
item, usable, response = int(full_name), True, None
|
||||
elif game in self.ctx.all_item_and_group_names:
|
||||
item, usable, response = get_intended_text(full_name, self.ctx.all_item_and_group_names[game])
|
||||
else:
|
||||
self.output("Can't look up item for unknown game. Hint for ID instead.")
|
||||
return False
|
||||
|
||||
if usable:
|
||||
if item_name in self.ctx.item_name_groups[game]:
|
||||
if game in self.ctx.item_name_groups and item in self.ctx.item_name_groups[game]:
|
||||
hints = []
|
||||
for item_name_from_group in self.ctx.item_name_groups[game][item_name]:
|
||||
for item_name_from_group in self.ctx.item_name_groups[game][item]:
|
||||
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
|
||||
else: # item name
|
||||
hints = collect_hints(self.ctx, team, slot, item_name)
|
||||
else: # item name or id
|
||||
hints = collect_hints(self.ctx, team, slot, item)
|
||||
|
||||
if hints:
|
||||
notify_hints(self.ctx, team, hints)
|
||||
@@ -1887,11 +1921,22 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||
location_name = " ".join(location_name)
|
||||
location_name, usable, response = get_intended_text(location_name,
|
||||
self.ctx.location_names_for_game(self.ctx.games[slot]))
|
||||
game = self.ctx.games[slot]
|
||||
full_name = " ".join(location_name)
|
||||
|
||||
if full_name.isnumeric():
|
||||
location, usable, response = int(full_name), True, None
|
||||
elif self.ctx.location_names_for_game(game) is not None:
|
||||
location, usable, response = get_intended_text(full_name, self.ctx.location_names_for_game(game))
|
||||
else:
|
||||
self.output("Can't look up location for unknown game. Hint for ID instead.")
|
||||
return False
|
||||
|
||||
if usable:
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, location_name)
|
||||
if isinstance(location, int):
|
||||
hints = collect_hint_location_id(self.ctx, team, slot, location)
|
||||
else:
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, location)
|
||||
if hints:
|
||||
notify_hints(self.ctx, team, hints)
|
||||
else:
|
||||
@@ -2041,15 +2086,28 @@ async def main(args: argparse.Namespace):
|
||||
args.auto_shutdown, args.compatibility, args.log_network)
|
||||
data_filename = args.multidata
|
||||
|
||||
try:
|
||||
if not data_filename:
|
||||
if not data_filename:
|
||||
try:
|
||||
filetypes = (("Multiworld data", (".archipelago", ".zip")),)
|
||||
data_filename = Utils.open_filename("Select multiworld data", filetypes)
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, ImportError) or (e.__class__.__name__ == "TclError" and "no display" in str(e)):
|
||||
if not isinstance(e, ImportError):
|
||||
logging.error(f"Failed to load tkinter ({e})")
|
||||
logging.info("Pass a multidata filename on command line to run headless.")
|
||||
exit(1)
|
||||
raise
|
||||
|
||||
if not data_filename:
|
||||
logging.info("No file selected. Exiting.")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
ctx.load(data_filename, args.use_embedded_options)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception('Failed to read multiworld data (%s)' % e)
|
||||
logging.exception(f"Failed to read multiworld data ({e})")
|
||||
raise
|
||||
|
||||
ctx.init_save(not args.disable_save)
|
||||
|
||||
115
Options.py
@@ -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:
|
||||
|
||||
@@ -15,9 +15,6 @@ import typing
|
||||
|
||||
from json import loads, dumps
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from Utils import init_logging, messagebox
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -19,10 +19,11 @@ from sc2.data import Race
|
||||
from sc2.main import run_game
|
||||
from sc2.player import Bot
|
||||
|
||||
import NetUtils
|
||||
from MultiServer import mark_raw
|
||||
from Utils import init_logging, is_windows
|
||||
from worlds.sc2wol import SC2WoLWorld
|
||||
from worlds.sc2wol.Items import lookup_id_to_name, item_table
|
||||
from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups
|
||||
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
|
||||
from worlds.sc2wol.MissionTables import lookup_id_to_mission
|
||||
from worlds.sc2wol.Regions import MissionInfo
|
||||
@@ -135,7 +136,7 @@ class SC2Context(CommonContext):
|
||||
last_loc_list = None
|
||||
difficulty_override = -1
|
||||
mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
|
||||
raw_text_parser: RawJSONtoTextParser
|
||||
last_bot: typing.Optional[ArchipelagoBot] = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SC2Context, self).__init__(*args, **kwargs)
|
||||
@@ -164,10 +165,13 @@ class SC2Context(CommonContext):
|
||||
check_mod_install()
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
# goes to this world
|
||||
if "receiving" in args and self.slot_concerns_self(args["receiving"]):
|
||||
relevant = True
|
||||
# found in this world
|
||||
elif "item" in args and self.slot_concerns_self(args["item"].player):
|
||||
relevant = True
|
||||
# not related
|
||||
else:
|
||||
relevant = False
|
||||
|
||||
@@ -291,34 +295,37 @@ class SC2Context(CommonContext):
|
||||
category_panel.add_widget(
|
||||
Label(text=category, size_hint_y=None, height=50, outline_width=1))
|
||||
|
||||
# Map is completed
|
||||
for mission in categories[category]:
|
||||
text = mission
|
||||
tooltip = ""
|
||||
text: str = mission
|
||||
tooltip: str = ""
|
||||
|
||||
# Map has uncollected locations
|
||||
if mission in unfinished_missions:
|
||||
text = f"[color=6495ED]{text}[/color]"
|
||||
|
||||
tooltip = f"Uncollected locations:\n"
|
||||
tooltip += "\n".join([self.ctx.location_names[loc] for loc in
|
||||
self.ctx.locations_for_mission(mission)
|
||||
if loc in self.ctx.missing_locations])
|
||||
elif mission in available_missions:
|
||||
text = f"[color=FFFFFF]{text}[/color]"
|
||||
# Map requirements not met
|
||||
else:
|
||||
text = f"[color=a9a9a9]{text}[/color]"
|
||||
tooltip = f"Requires: "
|
||||
if len(self.ctx.mission_req_table[mission].required_world) > 0:
|
||||
if self.ctx.mission_req_table[mission].required_world:
|
||||
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
|
||||
req_mission in
|
||||
self.ctx.mission_req_table[mission].required_world)
|
||||
|
||||
if self.ctx.mission_req_table[mission].number > 0:
|
||||
if self.ctx.mission_req_table[mission].number:
|
||||
tooltip += " and "
|
||||
if self.ctx.mission_req_table[mission].number > 0:
|
||||
if self.ctx.mission_req_table[mission].number:
|
||||
tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed"
|
||||
remaining_location_names: typing.List[str] = [
|
||||
self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
|
||||
if loc in self.ctx.missing_locations]
|
||||
if remaining_location_names:
|
||||
if tooltip:
|
||||
tooltip += "\n"
|
||||
tooltip += f"Uncollected locations:\n"
|
||||
tooltip += "\n".join(remaining_location_names)
|
||||
|
||||
mission_button = MissionButton(text=text, size_hint_y=None, height=50)
|
||||
mission_button.tooltip_text = tooltip
|
||||
@@ -355,6 +362,8 @@ class SC2Context(CommonContext):
|
||||
|
||||
async def shutdown(self):
|
||||
await super(SC2Context, self).shutdown()
|
||||
if self.last_bot:
|
||||
self.last_bot.want_close = True
|
||||
if self.sc2_run_task:
|
||||
self.sc2_run_task.cancel()
|
||||
|
||||
@@ -431,47 +440,27 @@ wol_default_categories = [
|
||||
]
|
||||
|
||||
|
||||
def calculate_items(items):
|
||||
unit_unlocks = 0
|
||||
armory1_unlocks = 0
|
||||
armory2_unlocks = 0
|
||||
upgrade_unlocks = 0
|
||||
building_unlocks = 0
|
||||
merc_unlocks = 0
|
||||
lab_unlocks = 0
|
||||
protoss_unlock = 0
|
||||
minerals = 0
|
||||
vespene = 0
|
||||
supply = 0
|
||||
def calculate_items(items: typing.List[NetUtils.NetworkItem]) -> typing.List[int]:
|
||||
network_item: NetUtils.NetworkItem
|
||||
accumulators: typing.List[int] = [0 for _ in type_flaggroups]
|
||||
|
||||
for item in items:
|
||||
data = lookup_id_to_name[item.item]
|
||||
for network_item in items:
|
||||
name: str = lookup_id_to_name[network_item.item]
|
||||
item_data: ItemData = item_table[name]
|
||||
|
||||
if item_table[data].type == "Unit":
|
||||
unit_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Upgrade":
|
||||
upgrade_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Armory 1":
|
||||
armory1_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Armory 2":
|
||||
armory2_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Building":
|
||||
building_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Mercenary":
|
||||
merc_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Laboratory":
|
||||
lab_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Protoss":
|
||||
protoss_unlock += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Minerals":
|
||||
minerals += item_table[data].number
|
||||
elif item_table[data].type == "Vespene":
|
||||
vespene += item_table[data].number
|
||||
elif item_table[data].type == "Supply":
|
||||
supply += item_table[data].number
|
||||
# exists exactly once
|
||||
if item_data.quantity == 1:
|
||||
accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number
|
||||
|
||||
return [unit_unlocks, upgrade_unlocks, armory1_unlocks, armory2_unlocks, building_unlocks, merc_unlocks,
|
||||
lab_unlocks, protoss_unlock, minerals, vespene, supply]
|
||||
# exists multiple times
|
||||
elif item_data.type == "Upgrade":
|
||||
accumulators[type_flaggroups[item_data.type]] += 1 << item_data.number
|
||||
|
||||
# sum
|
||||
else:
|
||||
accumulators[type_flaggroups[item_data.type]] += item_data.number
|
||||
|
||||
return accumulators
|
||||
|
||||
|
||||
def calc_difficulty(difficulty):
|
||||
@@ -502,7 +491,7 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
setup_done: bool
|
||||
ctx: SC2Context
|
||||
mission_id: int
|
||||
|
||||
want_close: bool = False
|
||||
can_read_game = False
|
||||
|
||||
last_received_update: int = 0
|
||||
@@ -510,12 +499,17 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
def __init__(self, ctx: SC2Context, mission_id):
|
||||
self.setup_done = False
|
||||
self.ctx = ctx
|
||||
self.ctx.last_bot = self
|
||||
self.mission_id = mission_id
|
||||
self.boni = [False for _ in range(max_bonus)]
|
||||
|
||||
super(ArchipelagoBot, self).__init__()
|
||||
|
||||
async def on_step(self, iteration: int):
|
||||
if self.want_close:
|
||||
self.want_close = False
|
||||
await self._client.leave()
|
||||
return
|
||||
game_state = 0
|
||||
if not self.setup_done:
|
||||
self.setup_done = True
|
||||
@@ -799,7 +793,12 @@ def check_game_install_path() -> bool:
|
||||
with open(einfo) as f:
|
||||
content = f.read()
|
||||
if content:
|
||||
base = re.search(r" = (.*)Versions", content).group(1)
|
||||
try:
|
||||
base = re.search(r" = (.*)Versions", content).group(1)
|
||||
except AttributeError:
|
||||
sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then "
|
||||
f"try again.")
|
||||
return False
|
||||
if os.path.exists(base):
|
||||
executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions")
|
||||
|
||||
@@ -816,7 +815,8 @@ def check_game_install_path() -> bool:
|
||||
else:
|
||||
sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
|
||||
else:
|
||||
sc2_logger.warning(f"Couldn't find {einfo}. Please run /set_path with your SC2 install directory.")
|
||||
sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. "
|
||||
f"If that fails, please run /set_path with your SC2 install directory.")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import websockets
|
||||
import asyncio
|
||||
import collections
|
||||
import datetime
|
||||
import functools
|
||||
import logging
|
||||
import pickle
|
||||
import random
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
import pickle
|
||||
import logging
|
||||
import datetime
|
||||
import websockets
|
||||
|
||||
import Utils
|
||||
from .models import db_session, Room, select, commit, Command, db
|
||||
@@ -49,6 +50,8 @@ class DBCommandProcessor(ServerCommandProcessor):
|
||||
|
||||
|
||||
class WebHostContext(Context):
|
||||
room_id: int
|
||||
|
||||
def __init__(self, static_server_data: dict):
|
||||
# static server data is used during _load_game_data to load required data,
|
||||
# without needing to import worlds system, which takes quite a bit of memory
|
||||
@@ -62,6 +65,8 @@ class WebHostContext(Context):
|
||||
def _load_game_data(self):
|
||||
for key, value in self.static_server_data.items():
|
||||
setattr(self, key, value)
|
||||
self.forced_auto_forfeits = collections.defaultdict(lambda: False, self.forced_auto_forfeits)
|
||||
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
||||
|
||||
def listen_to_db_commands(self):
|
||||
cmdprocessor = DBCommandProcessor(self)
|
||||
|
||||
343
docs/adding games.md
Normal file
@@ -0,0 +1,343 @@
|
||||
|
||||
|
||||
# How do I add a game to Archipelago?
|
||||
This guide is going to try and be a broad summary of how you can do just that.
|
||||
There are two key steps to incorporating a game into Archipelago:
|
||||
- Game Modification
|
||||
- Archipelago Server Integration
|
||||
|
||||
Refer to the following documents as well:
|
||||
- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) for network communication between client and server.
|
||||
- [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md) for documentation on server side code and creating a world package.
|
||||
|
||||
|
||||
# Game Modification
|
||||
One half of the work required to integrate a game into Archipelago is the development of the game client. This is
|
||||
typically done through a modding API or other modification process, described further down.
|
||||
|
||||
As an example, modifications to a game typically include (more on this later):
|
||||
- Hooking into when a 'location check' is completed.
|
||||
- Networking with the Archipelago server.
|
||||
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
|
||||
|
||||
In order to determine how to modify a game, refer to the following sections.
|
||||
|
||||
## Engine Identification
|
||||
This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
|
||||
Examples are provided below.
|
||||
|
||||
### Creepy Castle
|
||||

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

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

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

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

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

|
||||
|
||||
This file will contain the data of the actual game.
|
||||
For other C# games, the file you want is usually just the executable itself.
|
||||
|
||||
With dnSpy, you can view the game’s C# code, but the tool isn’t perfect.
|
||||
Although the names of classes, methods, variables, and more will be preserved, code structures may not remain entirely intact. This is because compilers will often subtly rewrite code to be more optimal, so that it works the same as the original code but uses fewer resources. Compiled C# files also lose comments and other documentation.
|
||||
|
||||
#### [UndertaleModTool](https://github.com/krzys-h/UndertaleModTool/releases)
|
||||
This is currently the best tool for modifying games made in GameMaker, and supports games made in both GMS 1 and 2.
|
||||
It allows you to modify code in GML, if the game wasn't made with the wrong compiler (usually something you don't have
|
||||
to worry about).
|
||||
|
||||
You'll want to open the data.win file, as this is where all the goods are kept.
|
||||
Like dnSpy, you won’t be able to see comments.
|
||||
In addition, you will be able to see and modify many hidden fields on items that GameMaker itself will often hide from
|
||||
creators.
|
||||
|
||||
Fonts in particular are notoriously complex, and to add new sprites you may need to modify existing sprite sheets.
|
||||
|
||||
#### [CheatEngine](https://cheatengine.org/)
|
||||
CheatEngine is a tool with a very long and storied history.
|
||||
Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
|
||||
malware (because this behavior is most commonly found in malware and rarely used by other programs).
|
||||
If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
|
||||
including binary data formats, addressing, and assembly language programming.
|
||||
|
||||
The tool itself is highly complex and even I have not yet charted its expanses.
|
||||
However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever
|
||||
modifying the actual game itself.
|
||||
In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
|
||||
anything with it.
|
||||
|
||||
### What Modifications You Should Make to the Game
|
||||
We talked about this briefly in [Game Modification](#game-modification) section.
|
||||
The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
|
||||
- Modify the game so that checks are shuffled
|
||||
- Know when the player has completed a check, and react accordingly
|
||||
- Listen for messages from the Archipelago server
|
||||
- Modify the game to display messages from the Archipelago server
|
||||
- Add interface for connecting to the Archipelago server with passwords and sessions
|
||||
- Add commands for manually rewarding, re-syncing, forfeiting, and other actions
|
||||
|
||||
To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive
|
||||
from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary,
|
||||
avoid double-awarding items while still maintaining game file integrity, and allow players to manually enter commands in
|
||||
case the client or server make mistakes.
|
||||
|
||||
Refer to the [Network Protocol documentation](./network%20protocol.md) for how to communicate with Archipelago's servers.
|
||||
|
||||
## But my Game is a console game. Can I still add it?
|
||||
That depends – what console?
|
||||
|
||||
### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
|
||||
Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
|
||||
that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
|
||||
holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console games.
|
||||
|
||||
### My Game isn’t that old, it’s for the Wii/PS2/360/etc
|
||||
This is very complex, but doable.
|
||||
If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
|
||||
There exist many disassembly and debugging tools, but more recent content may have lackluster support.
|
||||
|
||||
### My Game is a classic for the SNES/Sega Genesis/etc
|
||||
That’s a lot more feasible.
|
||||
There are many good tools available for understanding and modifying games on these older consoles, and the emulation
|
||||
community will have figured out the bulk of the console’s secrets.
|
||||
Look for debugging tools, but be ready to learn assembly.
|
||||
Old consoles usually have their own unique dialects of ASM you’ll need to get used to.
|
||||
|
||||
Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these
|
||||
older consoles to the Internet.
|
||||
There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a computer,
|
||||
but these will require the same sort of interface software to be written in order to work properly - from your perspective
|
||||
the two won't really look any different.
|
||||
|
||||
### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that-
|
||||
Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
|
||||
Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be
|
||||
working from scratch.
|
||||
|
||||
## How to Distribute Game Modifications
|
||||
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
|
||||
|
||||
This is a good way to get any project you're working on sued out from under you.
|
||||
The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you
|
||||
to copy them wholesale, is as patches.
|
||||
|
||||
There are many patch formats, which I'll cover in brief. The common theme is that you can’t distribute anything that
|
||||
wasn't made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding
|
||||
the issue of distributing someone else’s original work.
|
||||
|
||||
Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play.
|
||||
|
||||
### Patches
|
||||
|
||||
#### IPS
|
||||
IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
|
||||
moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
|
||||
fine.
|
||||
|
||||
#### UPS, BPS, VCDIFF (xdelta), bsdiff
|
||||
Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
|
||||
possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
|
||||
compression, so this format is used by APBP.
|
||||
|
||||
Only a bsdiff module is integrated into AP. If the final patch requires or is based on any other patch, convert them to
|
||||
bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
|
||||
|
||||
#### APBP Archipelago Binary Patch
|
||||
Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
|
||||
files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
|
||||
bsdiff between the original and the randomized ROM.
|
||||
|
||||
To make using APBP easy, they can be generated by inheriting from `Patch.APDeltaPatch`.
|
||||
|
||||
### Mod files
|
||||
Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere.
|
||||
Mod files come in many forms, but the rules about not distributing other people's content remain the same.
|
||||
They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
|
||||
generated per seed.
|
||||
|
||||
If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
|
||||
integration into the Webhost by inheriting from `Patch.APContainer`.
|
||||
|
||||
|
||||
## Archipelago Integration
|
||||
Integrating a randomizer into Archipelago involves a few steps.
|
||||
There are several things that may need to be done, but the most important is to create an implementation of the
|
||||
`World` class specific to your game. This implementation should exist as a Python module within the `worlds` folder
|
||||
in the Archipelago file structure.
|
||||
|
||||
This encompasses most of the data for your game – the items available, what checks you have, the logic for reaching those
|
||||
checks, what options to offer for the player’s yaml file, and the code to initialize all this data.
|
||||
|
||||
Here’s an example of what your world module can look like:
|
||||
|
||||

|
||||
|
||||
The minimum requirements for a new archipelago world are the package itself (the world folder containing a file named `__init__.py`),
|
||||
which must define a `World` class object for the game with a game name, create an equal number of items and locations with rules,
|
||||
a win condition, and at least one `Region` object.
|
||||
|
||||
Let's give a quick breakdown of what the contents for these files look like.
|
||||
This is just one example of an Archipelago world - the way things are done below is not an immutable property of Archipelago.
|
||||
|
||||
### Items.py
|
||||
This file is used to define the items which exist in a given game.
|
||||
|
||||

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

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

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

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

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

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

|
||||
|
||||
The minimum requirements for a new archipelago world are the package itself (the world folder containing a file named `__init__.py`),
|
||||
which must define a `World` class object for the game with a game name, create an equal number of items and locations with rules,
|
||||
a win condition, and at least one `Region` object.
|
||||
|
||||
Let's give a quick breakdown of what the contents for these files look like.
|
||||
This is just one example of an Archipelago world - the way things are done below is not an immutable property of Archipelago.
|
||||
|
||||
## Items.py
|
||||
This file is used to define the items which exist in a given game.
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
This is the most important file for the implementation, and technically the only one you need, but it's best to keep this
|
||||
file as short as possible and use other script files to do most of the heavy lifting.
|
||||
If you've done things well, this will just be where you assign everything you set up in the other files to their associated
|
||||
fields in the class being extended.
|
||||
|
||||
This is also a good place to put game-specific quirky behavior that needs to be managed, as it tends to make things a bit
|
||||
cluttered if you put these things elsewhere.
|
||||
|
||||
The various methods and attributes are documented in `/worlds/AutoWorld.py[World]` and
|
||||
[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md),
|
||||
though it is also recommended to look at existing implementations to see how all this works first-hand.
|
||||
Once you get all that, all that remains to do is test the game and publish your work.
|
||||
@@ -1,225 +0,0 @@
|
||||
# Adding Games to Archipelago
|
||||
This guide is going to try and be a broad summary of what is required to add a game integration to Archipelago.
|
||||
|
||||
This guide is not an in-depth tutorial on video game modification nor is it a getting started guide to software or
|
||||
video game development. The intent is to provide information, tips, and tools, to assist a would-be modder in adding a
|
||||
game integration to Archipelago.
|
||||
|
||||
There are two key steps to incorporating a game into Archipelago:
|
||||
- Game Modification
|
||||
- Archipelago Server Integration
|
||||
|
||||
This document covers game modification. Information on creating the Archipelago server integration may be found in the
|
||||
[Adding Archipelago Integration](./AddingArchipelagoIntegration.md).
|
||||
|
||||
## Game Modification
|
||||
One half of the work required to integrate a game into Archipelago is the development of the game client. This is
|
||||
typically done through a modding API or other modification process, this is described further down.
|
||||
|
||||
As an example, modifications to a game typically include:
|
||||
- Hooking into when a "location check" is completed.
|
||||
- Networking with the Archipelago server.
|
||||
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
|
||||
|
||||
### Engine Identification
|
||||
This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is
|
||||
critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s
|
||||
important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
|
||||
|
||||
#### Examples
|
||||
##### Proprietary Game Engine
|
||||

|
||||
|
||||
This is the game _Creepy Castle_. It’s your worst-case scenario as a modder. All that’s present here is an executable
|
||||
file and some meta-information that Steam uses. You have basically nothing here to work with. If you want to change
|
||||
this game, the only option you have is to do some pretty nasty disassembly and reverse engineering work, which is
|
||||
outside the scope of this tutorial.
|
||||
|
||||
##### Unity Game Engine
|
||||

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

|
||||
|
||||
The contents of the `HEAVY_BULLETS_Data` directory follow the pattern typically used by the Unity game engine.
|
||||
If you look in the sub-folders, you’ll seem some .dll files which affirm our suspicions. Telltale signs for this are
|
||||
directories titled `Managed` and `Mono`, as well as the numbered, extension-less level files and the sharedassets files.
|
||||
Also keep your eyes out for an executable with a name like UnityCrashHandler, that’s another dead giveaway.
|
||||
|
||||
##### XNA/FNA
|
||||

|
||||
|
||||
This is the game contents of _Stardew Valley_.
|
||||
A lot more to look at here, but there are some key takeaways. Notice the .dll files which include “CSharp” in their
|
||||
name. Also notice the `Content`. These signs point to a game based on the .NET framework and many games following this
|
||||
style will use the XNA game framework as the base to build their game from.
|
||||
|
||||
##### Gato Roboto
|
||||

|
||||
|
||||
Our last example is the game _Gato Roboto_. Notice the file titled `data.win`. This immediately tips us off that this
|
||||
game was made in GameMaker.
|
||||
|
||||
### Open or Leaked Source Games
|
||||
As a side note, many games have either been made open source, or have had source files leaked at some point.
|
||||
This can be a boon to any would-be modder, for obvious reasons.
|
||||
Always be sure to check - a quick internet search for "(Game) Source Code" might not give results often, but when it
|
||||
does you're going to have a much better time.
|
||||
|
||||
Be sure **never** to distribute source code for games that you decompile or find if you do not have express permission
|
||||
from the author to do so, nor to redistribute any materials obtained through similar methods, as this is illegal and
|
||||
unethical.
|
||||
|
||||
### Modifying Release Versions of Games
|
||||
Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools, but these are
|
||||
often not geared to the kind of work you'll be doing and may not help much. This is usually assessed on a case-by-case
|
||||
basis. Games with large modding communities typically grow around the tooling a developer provides or they grow around
|
||||
the fact that the game is easy to modify in the first place.
|
||||
|
||||
As a general rule, any modding tool that lets you write actual code is something worth using.
|
||||
|
||||
### Creating the Mod
|
||||
#### Research
|
||||
The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
|
||||
it's possible other motivated parties have concocted useful tools for your game already.
|
||||
Always be sure to search the Internet for the efforts of other modders.
|
||||
|
||||
#### Analysis Tools
|
||||
Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to
|
||||
existing game tools.
|
||||
|
||||
##### ILSpy
|
||||
You can download ILSpy and see more info about it on the [ILSpy GitHub repository homepage](https://github.com/icsharpcode/ILSpy).
|
||||
|
||||
The first tool in your toolbox is ILSpy. ILSpy is a .NET decompiler. The purpose of this program is to take a compiled
|
||||
.NET assembly (.DLL or .EXE file) and turn it back into human-readable source code. A file is a .NET assembly when it
|
||||
was created through the compilation of any programming language targeting the .NET runtime. Usually, the programming
|
||||
language in question is C# (C Sharp).
|
||||
|
||||
Unity games are a combination of native code (compiled in a "native language" such as C++) and .NET code. Most game
|
||||
developers will write the bulk of their code as C# and this will be compiled by Unity into .NET assemblies. Those files
|
||||
may then be decompiled with ILSpy to allow you to see the original source code of the game.
|
||||
|
||||
For Unity games, the file you’ll typically want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as
|
||||
pictured below:
|
||||
|
||||

|
||||
|
||||
For other .NET based games, which are not made in Unity, the file you want is usually just the executable itself.
|
||||
|
||||
Although the names of classes, methods, variables, and more will be preserved, code structures may not remain entirely
|
||||
intact. This is because compilers will often subtly rewrite code to be more optimal, so that it works the same as the
|
||||
original code but uses fewer resources. Compiled C# files also lose comments and other documentation.
|
||||
|
||||
##### UndertaleModTool
|
||||
You can download and find more info on UndertaleModTool on the [UndertaleModTool GitHub repository homepage](https://github.com/krzys-h/UndertaleModTool/releases).
|
||||
This is currently the best tool for modifying games made in GameMaker, and supports games made in both GameMaker Studio
|
||||
1 and 2. It allows you to modify code in GameMaker Language (GML).
|
||||
|
||||
Use the tool to open the `data.win` file to see game data and code.
|
||||
Like ILSpy, you won’t be able to see comments.
|
||||
In addition, you will be able to see and modify many hidden fields on items that GameMaker itself will often hide from
|
||||
creators.
|
||||
|
||||
Fonts in particular are notoriously complex, and to add new sprites you may need to modify existing sprite sheets.
|
||||
|
||||
#### What Modifications You Should Make to the Game
|
||||
We talked about this briefly in [Game Modification](#game-modification) section.
|
||||
The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
|
||||
- Modify the game so that checks are shuffled
|
||||
- Know when the player has completed a check, and react accordingly
|
||||
- Listen for messages from the Archipelago server
|
||||
- Modify the game to display messages from the Archipelago server
|
||||
- Add interface for connecting to the Archipelago server with passwords and sessions
|
||||
- Add commands for manually rewarding, re-syncing, forfeiting, and other actions
|
||||
|
||||
To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive
|
||||
from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary,
|
||||
avoid double-awarding items while still maintaining game file integrity, and allow players to manually enter commands in
|
||||
case the client or server make mistakes.
|
||||
|
||||
Refer to the [Network Protocol documentation](../NetworkProtocol.md) for how to communicate with Archipelago's servers.
|
||||
|
||||
### Modifying Console Games
|
||||
#### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
|
||||
Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
|
||||
that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
|
||||
holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console
|
||||
games.
|
||||
|
||||
There is some traction on this changing as studios are finding ways to include game modifications in console games.
|
||||
|
||||
#### My Game isn’t that old, it’s for the Wii/PS2/360/etc
|
||||
This is very complex, but doable.
|
||||
It is typically necessary to use Assembly (ASM) to modify these games.
|
||||
If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
|
||||
There exist many disassembly and debugging tools, but more recent content may have lackluster support.
|
||||
|
||||
#### My Game is a classic for the SNES/Sega Genesis/etc
|
||||
That’s a lot more feasible.
|
||||
There are many good tools available for understanding and modifying games on these older consoles, and the emulation
|
||||
community will have figured out the bulk of the console’s secrets. Look for debugging tools, but be ready to learn
|
||||
assembly. Old consoles usually have their own unique dialects of ASM you’ll need to get used to.
|
||||
|
||||
Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these
|
||||
older consoles to the internet. There are also hardware mods and flash carts, which can do the same things an emulator
|
||||
would when connected to a computer. These will require the same sort of interface software to be written in order to
|
||||
work properly--from your perspective the two won't really look any different.
|
||||
|
||||
#### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that-
|
||||
Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
|
||||
Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be
|
||||
working from scratch. You're welcome to try and break ground on something like this, but understand that community
|
||||
support will range from "extremely limited" to "nonexistent".
|
||||
|
||||
### How to Distribute Game Modifications
|
||||
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
|
||||
|
||||
The right way to distribute modified versions of a game's binaries is to distribute binary patches.
|
||||
|
||||
The common theme is that you can’t distribute anything that wasn't made by you. Patches are files that describe how
|
||||
your modified file differs from the original one without including the original file's content, thus avoiding the issue
|
||||
of distributing someone else’s original work.
|
||||
|
||||
Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play.
|
||||
|
||||
#### Patches
|
||||
The following patch formats are commonly seen in the game modding scene.
|
||||
|
||||
##### IPS
|
||||
IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
|
||||
moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
|
||||
fine.
|
||||
|
||||
##### UPS, BPS, VCDIFF (xdelta), bsdiff
|
||||
Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
|
||||
possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
|
||||
compression, so this format is used by APBP.
|
||||
|
||||
Only a bsdiff module is integrated into AP. If the final patch requires or is based on any other patch, convert them to
|
||||
bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
|
||||
|
||||
##### APBP Archipelago Binary Patch
|
||||
Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
|
||||
files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
|
||||
bsdiff between the original and the randomized ROM.
|
||||
|
||||
To make using APBP easy, they can be generated by inheriting from `Patch.APDeltaPatch`.
|
||||
|
||||
#### Mod files
|
||||
Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere.
|
||||
Mod files come in many forms, but the rules about not distributing other people's content remain the same.
|
||||
They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
|
||||
generated per seed.
|
||||
|
||||
If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
|
||||
integration into the Webhost by inheriting from `Patch.APContainer`.
|
||||
@@ -1,101 +0,0 @@
|
||||
# Archipelago Architecture
|
||||
Archipelago is split into several components. All components must operate in tandem to facilitate randomization
|
||||
and gameplay.
|
||||
|
||||
The components are:
|
||||
* [Archipelago Generator](#archipelago-generator)
|
||||
* [Archipelago Server](#archipelago-server)
|
||||
* [Archipelago Game Client](#archipelago-game-client)
|
||||
|
||||
Some games require additional components in order to facilitate gameplay or communication with Archipelago.
|
||||
The additional components vary from game to game but are typically:
|
||||
* [Retro Console Emulator](#retro-console-emulator)
|
||||
* [Emulator Communication Bridge (SNI)](#emulator-communication-bridge)
|
||||
|
||||
## Archipelago Generator
|
||||
The Archipelago Generator is the part of Archipelago which takes YAML configuration files as input and produces a ZIP
|
||||
file containing the data necessary for the Archipelago Server to service a session. The generator software is standalone
|
||||
from the server or game clients and is run outside the server context. The server may then be pointed to the resulting
|
||||
file to serve that session.
|
||||
|
||||
For more information on using the Archipelago Generator as a user, please visit the user facing MultiWorld Setup Guide
|
||||
section on [Rolling a YAML Locally](https://archipelago.gg/tutorial/Archipelago/setup/en#rolling-a-yaml-locally).
|
||||
|
||||
The Generator functions by using the classes defined in the `/worlds` folder to understand each game's items, location,
|
||||
YAML options, and logic. The "World" classes define these properties in code and are loaded by the generator to allow it
|
||||
to validate YAML options and create a multiworld with cohesive and solvable logic despite the possibility of disparate
|
||||
games being played.
|
||||
|
||||
## Archipelago Server
|
||||
The Archipelago Server facilitates gameplay for a multiworld session. A session may have any number of players.
|
||||
As Archipelago is client-server software the server is still required for sessions even if only a single player is
|
||||
present. The server takes a ZIP file or an ARCHIPELAGO file as input and serves the session using the information from
|
||||
the input to properly serve the game clients over network.
|
||||
|
||||
## Archipelago Game Client
|
||||
Archipelago game clients are currently implemented in two main ways. The first are in-process clients, which operate as
|
||||
a mod loaded within the game process. The game process will then facilitate the WebSocket communication with the
|
||||
Archipelago Server. Typically, more "modern" games will use this approach as they are typically easier to mod or are
|
||||
easier to inject with code at runtime.
|
||||
|
||||
Some examples of Archipelago games implementing the in-process model are:
|
||||
* [Risk of Rain 2](https://github.com/Ijwu/Archipelago.RiskOfRain2)
|
||||
* [Subnautica](https://github.com/Berserker66/ArchipelagoSubnauticaModSrc)
|
||||
* [Hollow Knight](https://github.com/Ijwu/Archipelago.HollowKnight)
|
||||
|
||||
The in-process model can be visualized using the following diagram:
|
||||
```{mermaid}
|
||||
flowchart LR
|
||||
APS[Archipelago Server]
|
||||
APGC[Archipelago Game Client]
|
||||
|
||||
APS <-- WebSockets --> APGC
|
||||
```
|
||||
|
||||
The second model of game client are those which operate out-of-process. The out-of-process clients are shipped with the
|
||||
Archipelago installation and live within the Archipelago codebase. They are implemented in Python using [CommonClient.py](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py)
|
||||
as a base. This client model is typically used for games in which runtime modification is difficult to impossible and for
|
||||
games which require additional components such as the emulator communication bridge. This model is also used for clients
|
||||
which communicate with the game from outside the game process to understand game state; the client then communicates
|
||||
updates to the Archipelago server based on the game state.
|
||||
|
||||
Some examples of Archipelago games implementing the out-of-process model are:
|
||||
* [Starcraft 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/Starcraft2Client.py)
|
||||
* [Factorio](https://github.com/ArchipelagoMW/Archipelago/blob/main/FactorioClient.py)
|
||||
* [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Archipelago/blob/main/OoTClient.py)
|
||||
|
||||
The out-of-process model can be visualized using the following diagram:
|
||||
```{mermaid}
|
||||
flowchart LR
|
||||
APS[Archipelago Server]
|
||||
OOPGC[Out-of-Process Game Client]
|
||||
GP[Game Process]
|
||||
|
||||
APS <-- WebSockets --> OOPGC <--> GP
|
||||
```
|
||||
|
||||
Games which use the [SNI](https://github.com/alttpo/sni) emulator communication bridge can be connected to Archipelago using the [SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py).
|
||||
|
||||
Games communicating using SNI may be visualized using the following diagram:
|
||||
```{mermaid}
|
||||
flowchart LR
|
||||
APS[Archipelago Server]
|
||||
SNIC[SNIClient]
|
||||
SNI[SNI]
|
||||
GP[Game Process]
|
||||
|
||||
APS <-- WebSockets --> SNIC <-- WebSockets --> SNI <--> GP
|
||||
```
|
||||
|
||||
## Retro Console Emulator
|
||||
Some game implementations require the use of an emulator in order to run the game and to communicate with SNI.
|
||||
These games are typically "retro" games which were released on 8-bit or 16-bit consoles, although newer consoles may be
|
||||
included for some game implementations.
|
||||
|
||||
All emulators currently used in Archipelago game implementations which require them are lua enabled and use a lua script
|
||||
to communicate with SNI.
|
||||
|
||||
## Emulator Communication Bridge
|
||||
All implementations of game clients for which the game is run in an emulator presently use [SuperNintendoInterface or SNI](https://github.com/alttpo/sni)
|
||||
to communicate between the emulator and the SNIClient. The emulator uses lua to communicate to SNI which communicates with
|
||||
the SNIClient which communicates with the Archipelago server.
|
||||
@@ -1,19 +0,0 @@
|
||||
World Class
|
||||
===========
|
||||
|
||||
```{eval-rst}
|
||||
.. currentmodule:: worlds.AutoWorld
|
||||
.. autoclass:: World
|
||||
:members: options, game, topology_present, all_item_and_group_names,
|
||||
item_name_to_id, location_name_to_id, item_name_groups, data_version,
|
||||
required_client_version, required_server_version, hint_blacklist,
|
||||
remote_items, remote_start_inventory, forced_auto_forfeit, hidden,
|
||||
world, player, item_id_to_name, location_id_to_name, item_names,
|
||||
location_names, web, assert_generate, generate_early, create_regions,
|
||||
create_items, set_rules, generate_basic, pre_fill, fill_hook, post_fill,
|
||||
generate_output, fill_slot_data, modify_multidata, write_spoiler_header,
|
||||
write_spoiler, write_spoiler_end, create_item, get_filler_item_name,
|
||||
collect_item, get_pre_fill_items
|
||||
:undoc-members:
|
||||
:special-members: __init__
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
@@ -1,56 +0,0 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Archipelago'
|
||||
copyright = '2022, Archipelago Team and Contributors'
|
||||
author = 'Archipelago Team and Contributors'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
"myst_parser",
|
||||
"sphinxcontrib.mermaid",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.autosummary",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
myst_heading_anchors = 4
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
@@ -1,70 +0,0 @@
|
||||
% Archipelago documentation master file, created by
|
||||
% sphinx-quickstart on Wed Jul 6 20:09:51 2022.
|
||||
% You can adapt this file completely to your liking, but it should at least
|
||||
% contain the root `toctree` directive.
|
||||
|
||||
Welcome to Archipelago's Technical Documentation!
|
||||
=================================================
|
||||
|
||||
## What is Archipelago?
|
||||
Archipelago provides a generic framework for developing multiworld capability for game randomizers.
|
||||
In all cases, presently, Archipelago is also the randomizer itself.
|
||||
|
||||
Archipelago is end-user facing software intended to facilitate randomizer and multiworld play for a variety of
|
||||
supported games.
|
||||
|
||||
Archipelago presently supports the following games:
|
||||
* The Legend of Zelda: A Link to the Past
|
||||
* Factorio
|
||||
* Minecraft
|
||||
* Subnautica
|
||||
* Slay the Spire
|
||||
* Risk of Rain 2
|
||||
* The Legend of Zelda: Ocarina of Time
|
||||
* Timespinner
|
||||
* Super Metroid
|
||||
* Secret of Evermore
|
||||
* Final Fantasy
|
||||
* Rogue Legacy
|
||||
* VVVVVV
|
||||
* Raft
|
||||
* Super Mario 64
|
||||
* Meritous
|
||||
* Super Metroid/Link to the Past combo randomizer (SMZ3)
|
||||
* ChecksFinder
|
||||
* ArchipIDLE
|
||||
* Hollow Knight
|
||||
* The Witness
|
||||
* Sonic Adventure 2: Battle
|
||||
* Starcraft 2: Wings of Liberty
|
||||
* Donkey Kong Country 3
|
||||
* Dark Souls 3
|
||||
|
||||
For more information on the technical architecture of Archipelago,
|
||||
please refer to the [Archipelago Technical Architecture](Architecture.md) document.
|
||||
|
||||
## Contributing to Archipelago
|
||||
Contributions to the Archipelago code are welcome and dearly appreciated. Contributions may occur as changes to website
|
||||
content, changes to Archipelago core code, additions of game integrations, or alterations to website functionality.
|
||||
|
||||
Please visit our [contributing guidelines on our GitHub README](https://github.com/ArchipelagoMW/Archipelago#contributing)
|
||||
for some more information on what may be expected.
|
||||
|
||||
For information on contributing a game integration, check out our [document on adding games to Archipelago](./AddingGames.md).
|
||||
|
||||
## Table of Contents
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
caption: "Documentation contents:"
|
||||
---
|
||||
AddingGames
|
||||
WorldAPI
|
||||
NetworkProtocol
|
||||
NetworkDiagram
|
||||
```
|
||||
|
||||
## Indices and tables
|
||||
* {ref}`genindex`
|
||||
* {ref}`modindex`
|
||||
* {ref}`search`
|
||||
@@ -1,35 +0,0 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.https://www.sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
||||
@@ -1,4 +0,0 @@
|
||||
sphinx==5.0.2
|
||||
sphinx_rtd_theme==1.0.0
|
||||
readthedocs-sphinx-search==0.1.2
|
||||
myst-parser==0.18
|
||||
@@ -6,20 +6,26 @@ required to send and receive items between the game and server.
|
||||
|
||||
Client implementation is out of scope of this document. Please refer to an
|
||||
existing game that provides a similar API to yours.
|
||||
Refer to the following documents as well:
|
||||
- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md)
|
||||
- [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md)
|
||||
|
||||
Archipelago will be abbreviated as "AP" from now on.
|
||||
|
||||
|
||||
## Language
|
||||
|
||||
AP worlds are written in python3.
|
||||
Clients that connect to the server to sync items can be in any language that
|
||||
allows using WebSockets.
|
||||
|
||||
|
||||
## Coding style
|
||||
|
||||
AP follows all the PEPs. When in doubt use an IDE with coding style
|
||||
linter, for example PyCharm Community Edition.
|
||||
|
||||
|
||||
## Docstrings
|
||||
|
||||
Docstrings are strings attached to an object in Python that describe what the
|
||||
@@ -34,6 +40,7 @@ class MyGameWorld(World):
|
||||
website."""
|
||||
```
|
||||
|
||||
|
||||
## Definitions
|
||||
|
||||
This section will cover various classes and objects you can use for your world.
|
||||
@@ -56,7 +63,7 @@ for your world specifically on the webhost.
|
||||
`theme` to be used for your game specific AP pages. Available themes:
|
||||
| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| <img src="_static/theme_dirt.JPG" width="200"> | <img src="_static/theme_grass.JPG" width="200"> | <img src="_static/theme_grassFlowers.JPG" width="200"> | <img src="_static/theme_ice.JPG" width="200"> | <img src="_static/theme_jungle.JPG" width="200"> | <img src="_static/theme_ocean.JPG" width="200"> | <img src="_static/theme_partyTime.JPG" width="200"> | <img src="_static/theme_stone.JPG" width="200"> |
|
||||
| <img src="img/theme_dirt.JPG" width="100"> | <img src="img/theme_grass.JPG" width="100"> | <img src="img/theme_grassFlowers.JPG" width="100"> | <img src="img/theme_ice.JPG" width="100"> | <img src="img/theme_jungle.JPG" width="100"> | <img src="img/theme_ocean.JPG" width="100"> | <img src="img/theme_partyTime.JPG" width="100"> | <img src="img/theme_stone.JPG" width="100"> |
|
||||
|
||||
`bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be placed by the site to help direct users to report bugs.
|
||||
|
||||
@@ -96,8 +103,9 @@ or boss drops for RPG-like games but could also be progress in a research tree.
|
||||
|
||||
Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed
|
||||
in a Region and has access rules.
|
||||
The name needs to be unique in each game, the ID needs to be unique across all
|
||||
games and is best in the same range as the item IDs.
|
||||
The name needs to be unique in each game and must not be numeric (has to
|
||||
contain least 1 letter or symbol). The ID needs to be unique across all games
|
||||
and is best in the same range as the item IDs.
|
||||
World-specific IDs are 1 to 2<sup>53</sup>-1, IDs ≤ 0 are global and reserved.
|
||||
|
||||
Special locations with ID `None` can hold events.
|
||||
@@ -114,6 +122,9 @@ their world. Progression items will be assigned to locations with higher
|
||||
priority and moved around to meet defined rules and accomplish progression
|
||||
balancing.
|
||||
|
||||
The name needs to be unique in each game, meaning a duplicate item has the
|
||||
same ID. Name must not be numeric (has to contain at least 1 letter or symbol).
|
||||
|
||||
Special items with ID `None` can mark events (read below).
|
||||
|
||||
Other classifications include
|
||||
@@ -181,15 +192,17 @@ the `/worlds` directory. The starting point for the package is `__init.py__`.
|
||||
Conventionally, your world class is placed in that file.
|
||||
|
||||
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
|
||||
which can be imported as `..AutoWorld.World` from your package.
|
||||
which can be imported as `worlds.AutoWorld.World` from your package.
|
||||
|
||||
AP will pick up your world automatically due to the `AutoWorld` implementation.
|
||||
|
||||
### Requirements
|
||||
|
||||
If your world needs specific python packages, they can be listed in
|
||||
`world/[world_name]/requirements.txt`.
|
||||
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format)
|
||||
`world/[world_name]/requirements.txt`. ModuleUpdate.py will automatically
|
||||
pick up and install them.
|
||||
|
||||
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format).
|
||||
|
||||
### Relative Imports
|
||||
|
||||
@@ -202,6 +215,10 @@ e.g. `from .Options import mygame_options` from your `__init__.py` will load
|
||||
When imported names pile up it may be easier to use `from . import Options`
|
||||
and access the variable as `Options.mygame_options`.
|
||||
|
||||
Imports from directories outside your world should use absolute imports.
|
||||
Correct use of relative / absolute imports is required for zipped worlds to
|
||||
function, see [apworld specification.md](apworld%20specification.md).
|
||||
|
||||
### Your Item Type
|
||||
|
||||
Each world uses its own subclass of `BaseClasses.Item`. The constuctor can be
|
||||
@@ -267,14 +284,12 @@ Define a property `option_<name> = <number>` per selectable value and
|
||||
`default = <number>` to set the default selection. Aliases can be set by
|
||||
defining a property `alias_<name> = <same number>`.
|
||||
|
||||
One special case where aliases are required is when option name is `yes`, `no`,
|
||||
`on` or `off` because they parse to `True` or `False`:
|
||||
```python
|
||||
option_off = 0
|
||||
option_on = 1
|
||||
option_some = 2
|
||||
alias_false = 0
|
||||
alias_true = 1
|
||||
alias_disabled = 0
|
||||
alias_enabled = 1
|
||||
default = 0
|
||||
```
|
||||
|
||||
@@ -316,7 +331,7 @@ mygame_options: typing.Dict[str, type(Option)] = {
|
||||
```python
|
||||
# __init__.py
|
||||
|
||||
from ..AutoWorld import World
|
||||
from worlds.AutoWorld import World
|
||||
from .Options import mygame_options # import the options dict
|
||||
|
||||
class MyGameWorld(World):
|
||||
@@ -345,7 +360,7 @@ more natural. These games typically have been edited to 'bake in' the items.
|
||||
from .Options import mygame_options # the options we defined earlier
|
||||
from .Items import mygame_items # data used below to add items to the World
|
||||
from .Locations import mygame_locations # same as above
|
||||
from ..AutoWorld import World
|
||||
from worlds.AutoWorld import World
|
||||
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
|
||||
from Utils import get_options, output_path
|
||||
|
||||
@@ -546,7 +561,7 @@ def generate_basic(self) -> None:
|
||||
### Setting Rules
|
||||
|
||||
```python
|
||||
from ..generic.Rules import add_rule, set_rule, forbid_item
|
||||
from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
||||
from Items import get_item_type
|
||||
|
||||
def set_rules(self) -> None:
|
||||
@@ -596,7 +611,7 @@ implement more complex logic in logic mixins, even if there is no need to add
|
||||
properties to the `BaseClasses.CollectionState` state object.
|
||||
|
||||
When importing a file that defines a class that inherits from
|
||||
`..AutoWorld.LogicMixin` the state object's class is automatically extended by
|
||||
`worlds.AutoWorld.LogicMixin` the state object's class is automatically extended by
|
||||
the mixin's members. These members should be prefixed with underscore following
|
||||
the name of the implementing world. This is due to sharing a namespace with all
|
||||
other logic mixins.
|
||||
@@ -615,7 +630,7 @@ Please do this with caution and only when neccessary.
|
||||
```python
|
||||
# Logic.py
|
||||
|
||||
from ..AutoWorld import LogicMixin
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
|
||||
class MyGameLogic(LogicMixin):
|
||||
def _mygame_has_key(self, world: MultiWorld, player: int):
|
||||
@@ -626,7 +641,7 @@ class MyGameLogic(LogicMixin):
|
||||
```python
|
||||
# __init__.py
|
||||
|
||||
from ..generic.Rules import set_rule
|
||||
from worlds.generic.Rules import set_rule
|
||||
import .Logic # apply the mixin by importing its file
|
||||
|
||||
class MyGameWorld(World):
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.")
|
||||
@@ -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")))
|
||||
|
||||
@@ -120,87 +120,67 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
A Game should have its own subclass of World in which it defines the required data structures."""
|
||||
|
||||
option_definitions: Dict[str, Option[Any]] = {}
|
||||
""" link your Options mapping """
|
||||
|
||||
game: str
|
||||
""" name of the game the world is for """
|
||||
|
||||
topology_present: bool = False
|
||||
""" indicate if world type has any meaningful layout/pathing """
|
||||
option_definitions: Dict[str, Option[Any]] = {} # link your Options mapping
|
||||
game: str # name the game
|
||||
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
|
||||
|
||||
# gets automatically populated with all item and item group names
|
||||
all_item_and_group_names: FrozenSet[str] = frozenset()
|
||||
""" gets automatically populated with all item and item group names """
|
||||
|
||||
# map names to their IDs
|
||||
item_name_to_id: Dict[str, int] = {}
|
||||
""" map item names to their IDs """
|
||||
|
||||
location_name_to_id: Dict[str, int] = {}
|
||||
""" map location names to their IDs """
|
||||
|
||||
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
|
||||
item_name_groups: Dict[str, Set[str]] = {}
|
||||
""" maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"} """
|
||||
|
||||
# increment this every time something in your world's names/id mappings changes.
|
||||
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
||||
# retrieved by clients on every connection.
|
||||
data_version: int = 1
|
||||
"""increment this every time something in your world's names/id mappings changes.
|
||||
While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
||||
retrieved by clients on every connection.
|
||||
"""
|
||||
|
||||
# override this if changes to a world break forward-compatibility of the client
|
||||
# The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
|
||||
# future. Protocol level compatibility check moved to MultiServer.min_client_version.
|
||||
required_client_version: Tuple[int, int, int] = (0, 1, 6)
|
||||
""" override this if changes to a world break forward-compatibility of the client
|
||||
The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
|
||||
future. Protocol level compatibility check moved to MultiServer.min_client_version.
|
||||
"""
|
||||
|
||||
# update this if the resulting multidata breaks forward-compatibility of the server
|
||||
required_server_version: Tuple[int, int, int] = (0, 2, 4)
|
||||
""" update this if the resulting multidata breaks forward-compatibility of the server """
|
||||
|
||||
hint_blacklist: FrozenSet[str] = frozenset()
|
||||
""" any names that should not be hintable """
|
||||
hint_blacklist: FrozenSet[str] = frozenset() # any names that should not be hintable
|
||||
|
||||
# NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set.
|
||||
# These values will be removed.
|
||||
# if a world is set to remote_items, then it just needs to send location checks to the server and the server
|
||||
# sends back the items
|
||||
# if a world is set to remote_items = False, then the server never sends an item where receiver == finder,
|
||||
# the client finds its own items in its own world.
|
||||
remote_items: bool = True
|
||||
""" NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set.
|
||||
These values will be removed.
|
||||
if a world is set to remote_items, then it just needs to send location checks to the server and the server
|
||||
sends back the items
|
||||
if a world is set to remote_items = False, then the server never sends an item where receiver == finder,
|
||||
the client finds its own items in its own world.
|
||||
"""
|
||||
|
||||
# If remote_start_inventory is true, the start_inventory/world.precollected_items is sent on connection,
|
||||
# otherwise the world implementation is in charge of writing the items to their output data.
|
||||
remote_start_inventory: bool = True
|
||||
""" If remote_start_inventory is true, the start_inventory/world.precollected_items is sent on connection,
|
||||
otherwise the world implementation is in charge of writing the items to their output data.
|
||||
"""
|
||||
|
||||
# For games where after a victory it is impossible to go back in and get additional/remaining Locations checked.
|
||||
# this forces forfeit: auto for those games.
|
||||
forced_auto_forfeit: bool = False
|
||||
""" For games where after a victory it is impossible to go back in and get additional/remaining Locations checked.
|
||||
this forces forfeit: auto for those games.
|
||||
"""
|
||||
|
||||
# Hide World Type from various views. Does not remove functionality.
|
||||
hidden: bool = False
|
||||
""" Hide World Type from various views. Does not remove functionality. """
|
||||
|
||||
# see WebWorld for options
|
||||
web: WebWorld = WebWorld()
|
||||
|
||||
# autoset on creation:
|
||||
world: "MultiWorld"
|
||||
""" autoset on creation """
|
||||
|
||||
player: int
|
||||
""" autoset on creation """
|
||||
|
||||
# automatically generated
|
||||
item_id_to_name: Dict[int, str]
|
||||
""" automatically generated inverse of item_name_to_id """
|
||||
|
||||
location_id_to_name: Dict[int, str]
|
||||
""" automatically generated inverse of location_name_to_id """
|
||||
|
||||
item_names: Set[str]
|
||||
""" set of all potential item names """
|
||||
|
||||
location_names: Set[str]
|
||||
""" set of all potential location names """
|
||||
item_names: Set[str] # set of all potential item names
|
||||
location_names: Set[str] # set of all potential location names
|
||||
|
||||
zip_path: Optional[pathlib.Path] = None # If loaded from a .apworld, this is the Path to it.
|
||||
__file__: str # path it was loaded from
|
||||
@@ -241,10 +221,8 @@ class World(metaclass=AutoWorldRegister):
|
||||
@classmethod
|
||||
def fill_hook(cls,
|
||||
progitempool: List["Item"],
|
||||
nonexcludeditempool: List["Item"],
|
||||
localrestitempool: Dict[int, List["Item"]],
|
||||
nonlocalrestitempool: Dict[int, List["Item"]],
|
||||
restitempool: List["Item"],
|
||||
usefulitempool: List["Item"],
|
||||
filleritempool: List["Item"],
|
||||
fill_locations: List["Location"]) -> None:
|
||||
"""Special method that gets called as part of distribute_items_restrictive (main fill).
|
||||
This gets called once per present world type."""
|
||||
@@ -262,6 +240,11 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""Fill in the slot_data field in the Connected network package."""
|
||||
return {}
|
||||
|
||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
|
||||
"""Fill in additional entrance information text into locations, which is displayed when hinted.
|
||||
structure is {player_id: {location_id: text}} You will need to insert your own player_id."""
|
||||
pass
|
||||
|
||||
def modify_multidata(self, multidata: Dict[str, Any]) -> None: # TODO: TypedDict for multidata?
|
||||
"""For deeper modification of server multidata."""
|
||||
pass
|
||||
@@ -304,8 +287,8 @@ class World(metaclass=AutoWorldRegister):
|
||||
return item.name
|
||||
return None
|
||||
|
||||
# called to create all_state, return Items that are created during pre_fill
|
||||
def get_pre_fill_items(self) -> List["Item"]:
|
||||
""" called to create all_state, return Items that are created during pre_fill """
|
||||
return []
|
||||
|
||||
# following methods should not need to be overridden.
|
||||
|
||||
@@ -5,15 +5,14 @@ import typing
|
||||
|
||||
folder = os.path.dirname(__file__)
|
||||
|
||||
__all__ = [
|
||||
__all__ = {
|
||||
"lookup_any_item_id_to_name",
|
||||
"lookup_any_location_id_to_name",
|
||||
"network_data_package",
|
||||
"AutoWorldRegister",
|
||||
"world_sources",
|
||||
"folder",
|
||||
"World"
|
||||
]
|
||||
}
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .AutoWorld import World
|
||||
@@ -28,7 +27,8 @@ class WorldSource(typing.NamedTuple):
|
||||
world_sources: typing.List[WorldSource] = []
|
||||
file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
|
||||
for file in os.scandir(folder):
|
||||
if not file.name.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders
|
||||
# prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
|
||||
if not file.name.startswith(("_", ".")):
|
||||
if file.is_dir():
|
||||
world_sources.append(WorldSource(file.name))
|
||||
elif file.is_file() and file.name.endswith(".apworld"):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 += [
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}}
|
||||
@@ -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"}}}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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) }}
|
||||
|
||||
@@ -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 -%}
|
||||
|
||||
1
worlds/factorio/data/mods.json
Normal file
@@ -0,0 +1 @@
|
||||
{"base":"1.1.69","archipelago-extractor":"1.1.1"}
|
||||
@@ -66,7 +66,10 @@ class FF1World(World):
|
||||
def goal_rule_and_shards(state):
|
||||
return goal_rule(state) and state.has("Shard", self.player, 32)
|
||||
terminated_event.access_rule = goal_rule_and_shards
|
||||
|
||||
if "MARK" in items.keys():
|
||||
# Fail generation for Noverworld and provide link to old FFR website
|
||||
raise Exception("FFR Noverworld seeds must be generated on an older version of FFR. Please ensure you generated the settings using "
|
||||
"4-4-0.finalfantasyrandomizer.com")
|
||||
menu_region.locations.append(terminated_event)
|
||||
self.world.regions += [menu_region]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -163,3 +163,17 @@ filler_items: typing.Tuple[str, ...] = (
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
|
||||
data.code}
|
||||
# Map type to expected int
|
||||
type_flaggroups: typing.Dict[str, int] = {
|
||||
"Unit": 0,
|
||||
"Upgrade": 1,
|
||||
"Armory 1": 2,
|
||||
"Armory 2": 3,
|
||||
"Building": 4,
|
||||
"Mercenary": 5,
|
||||
"Laboratory": 6,
|
||||
"Protoss": 7,
|
||||
"Minerals": 8,
|
||||
"Vespene": 9,
|
||||
"Supply": 10,
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -107,7 +107,6 @@ class HeartBeepSpeed(Choice):
|
||||
option_Half = 2
|
||||
option_Normal = 3
|
||||
option_Double = 4
|
||||
alias_false = 0
|
||||
default = 3
|
||||
|
||||
class HeartColor(Choice):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -139,7 +139,7 @@ item_table: Dict[int, ItemDict] = {
|
||||
'name': 'Power Transmitter Fragment',
|
||||
'tech_type': 'PowerTransmitterFragment'},
|
||||
35032: {'classification': ItemClassification.progression,
|
||||
'count': 4,
|
||||
'count': 5,
|
||||
'name': 'Prawn Suit Fragment',
|
||||
'tech_type': 'ExosuitFragment'},
|
||||
35033: {'classification': ItemClassification.useful,
|
||||
@@ -163,7 +163,7 @@ item_table: Dict[int, ItemDict] = {
|
||||
'name': 'Scanner Room Fragment',
|
||||
'tech_type': 'BaseMapRoomFragment'},
|
||||
35038: {'classification': ItemClassification.progression,
|
||||
'count': 5,
|
||||
'count': 4,
|
||||
'name': 'Seamoth Fragment',
|
||||
'tech_type': 'SeamothFragment'},
|
||||
35039: {'classification': ItemClassification.progression,
|
||||
@@ -203,9 +203,9 @@ item_table: Dict[int, ItemDict] = {
|
||||
'name': 'Picture Frame',
|
||||
'tech_type': 'PictureFrameFragment'},
|
||||
35048: {'classification': ItemClassification.filler,
|
||||
'count': 2,
|
||||
'name': 'Bench Fragment',
|
||||
'tech_type': 'BenchFragment'},
|
||||
'count': 1,
|
||||
'name': 'Bench',
|
||||
'tech_type': 'Bench'},
|
||||
35049: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Basic Plant Pot',
|
||||
@@ -333,7 +333,12 @@ item_table: Dict[int, ItemDict] = {
|
||||
35080: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Water Filtration Machine',
|
||||
'tech_type': 'BaseFiltrationMachine'}}
|
||||
'tech_type': 'BaseFiltrationMachine'},
|
||||
35081: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Ultra High Capacity Tank',
|
||||
'tech_type': 'HighCapacityTank'},
|
||||
}
|
||||
|
||||
advancement_item_names: Set[str] = set()
|
||||
non_advancement_item_names: Set[str] = set()
|
||||
|
||||
@@ -41,7 +41,7 @@ class SubnauticaWorld(World):
|
||||
location_name_to_id = all_locations
|
||||
option_definitions = Options.options
|
||||
|
||||
data_version = 6
|
||||
data_version = 7
|
||||
required_client_version = (0, 3, 5)
|
||||
|
||||
prefill_items: List[Item]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||