Merge branch 'main' into ror2
This commit is contained in:
commit
f16b29b16b
|
@ -27,6 +27,7 @@ class MultiWorld():
|
||||||
plando_connections: List
|
plando_connections: List
|
||||||
worlds: Dict[int, Any]
|
worlds: Dict[int, Any]
|
||||||
is_race: bool = False
|
is_race: bool = False
|
||||||
|
precollected_items: Dict[int, List[Item]]
|
||||||
|
|
||||||
class AttributeProxy():
|
class AttributeProxy():
|
||||||
def __init__(self, rule):
|
def __init__(self, rule):
|
||||||
|
@ -46,7 +47,7 @@ class MultiWorld():
|
||||||
self.itempool = []
|
self.itempool = []
|
||||||
self.seed = None
|
self.seed = None
|
||||||
self.seed_name: str = "Unavailable"
|
self.seed_name: str = "Unavailable"
|
||||||
self.precollected_items = []
|
self.precollected_items = {player: [] for player in self.player_ids}
|
||||||
self.state = CollectionState(self)
|
self.state = CollectionState(self)
|
||||||
self._cached_entrances = None
|
self._cached_entrances = None
|
||||||
self._cached_locations = None
|
self._cached_locations = None
|
||||||
|
@ -266,7 +267,7 @@ class MultiWorld():
|
||||||
|
|
||||||
def push_precollected(self, item: Item):
|
def push_precollected(self, item: Item):
|
||||||
item.world = self
|
item.world = self
|
||||||
self.precollected_items.append(item)
|
self.precollected_items[item.player].append(item)
|
||||||
self.state.collect(item, True)
|
self.state.collect(item, True)
|
||||||
|
|
||||||
def push_item(self, location: Location, item: Item, collect: bool = True):
|
def push_item(self, location: Location, item: Item, collect: bool = True):
|
||||||
|
@ -473,8 +474,9 @@ class CollectionState(object):
|
||||||
self.path = {}
|
self.path = {}
|
||||||
self.locations_checked = set()
|
self.locations_checked = set()
|
||||||
self.stale = {player: True for player in range(1, parent.players + 1)}
|
self.stale = {player: True for player in range(1, parent.players + 1)}
|
||||||
for item in parent.precollected_items:
|
for items in parent.precollected_items.values():
|
||||||
self.collect(item, True)
|
for item in items:
|
||||||
|
self.collect(item, True)
|
||||||
|
|
||||||
def update_reachable_regions(self, player: int):
|
def update_reachable_regions(self, player: int):
|
||||||
from worlds.alttp.EntranceShuffle import indirect_connections
|
from worlds.alttp.EntranceShuffle import indirect_connections
|
||||||
|
|
57
Main.py
57
Main.py
|
@ -1,4 +1,4 @@
|
||||||
from itertools import zip_longest
|
from itertools import zip_longest, chain
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
@ -7,7 +7,7 @@ import concurrent.futures
|
||||||
import pickle
|
import pickle
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple, Optional
|
||||||
|
|
||||||
from BaseClasses import MultiWorld, CollectionState, Region, RegionType
|
from BaseClasses import MultiWorld, CollectionState, Region, RegionType
|
||||||
from worlds.alttp.Items import item_name_groups
|
from worlds.alttp.Items import item_name_groups
|
||||||
|
@ -19,7 +19,16 @@ from worlds.generic.Rules import locality_rules, exclusion_rules
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
|
|
||||||
|
|
||||||
def main(args, seed=None):
|
ordered_areas = (
|
||||||
|
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||||
|
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
||||||
|
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
|
||||||
|
if not baked_server_options:
|
||||||
|
baked_server_options = get_options()["server_options"]
|
||||||
if args.outputpath:
|
if args.outputpath:
|
||||||
os.makedirs(args.outputpath, exist_ok=True)
|
os.makedirs(args.outputpath, exist_ok=True)
|
||||||
output_path.cached_path = args.outputpath
|
output_path.cached_path = args.outputpath
|
||||||
|
@ -30,7 +39,7 @@ def main(args, seed=None):
|
||||||
world = MultiWorld(args.multi)
|
world = MultiWorld(args.multi)
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
world.set_seed(secure=args.race, name=str(args.outputname if args.outputname else world.seed))
|
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))
|
||||||
|
|
||||||
world.shuffle = args.shuffle.copy()
|
world.shuffle = args.shuffle.copy()
|
||||||
world.logic = args.logic.copy()
|
world.logic = args.logic.copy()
|
||||||
|
@ -159,16 +168,15 @@ def main(args, seed=None):
|
||||||
|
|
||||||
output = tempfile.TemporaryDirectory()
|
output = tempfile.TemporaryDirectory()
|
||||||
with output as temp_dir:
|
with output as temp_dir:
|
||||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
|
||||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||||
|
|
||||||
output_file_futures = []
|
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
|
||||||
|
|
||||||
for player in world.player_ids:
|
for player in world.player_ids:
|
||||||
# skip starting a thread for methods that say "pass".
|
# skip starting a thread for methods that say "pass".
|
||||||
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
|
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
|
||||||
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
output_file_futures.append(
|
||||||
output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
|
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||||
|
|
||||||
def get_entrance_to_region(region: Region):
|
def get_entrance_to_region(region: Region):
|
||||||
for entrance in region.entrances:
|
for entrance in region.entrances:
|
||||||
|
@ -189,9 +197,7 @@ def main(args, seed=None):
|
||||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||||
er_hint_data[region.player][location.address] = main_entrance.name
|
er_hint_data[region.player][location.address] = main_entrance.name
|
||||||
|
|
||||||
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
|
||||||
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
|
||||||
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
|
|
||||||
|
|
||||||
checks_in_area = {player: {area: list() for area in ordered_areas}
|
checks_in_area = {player: {area: list() for area in ordered_areas}
|
||||||
for player in range(1, world.players + 1)}
|
for player in range(1, world.players + 1)}
|
||||||
|
@ -220,8 +226,9 @@ def main(args, seed=None):
|
||||||
for index, take_any in enumerate(takeanyregions):
|
for index, take_any in enumerate(takeanyregions):
|
||||||
for region in [world.get_region(take_any, player) for player in
|
for region in [world.get_region(take_any, player) for player in
|
||||||
world.get_game_players("A Link to the Past") if world.retro[player]]:
|
world.get_game_players("A Link to the Past") if world.retro[player]]:
|
||||||
item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
item = world.create_item(
|
||||||
region.player)
|
region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
||||||
|
region.player)
|
||||||
player = region.player
|
player = region.player
|
||||||
location_id = SHOP_ID_START + total_shop_slots + index
|
location_id = SHOP_ID_START + total_shop_slots + index
|
||||||
|
|
||||||
|
@ -246,18 +253,16 @@ def main(args, seed=None):
|
||||||
for slot in world.player_ids:
|
for slot in world.player_ids:
|
||||||
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
||||||
games[slot] = world.game[slot]
|
games[slot] = world.game[slot]
|
||||||
precollected_items = {player: [] for player in range(1, world.players + 1)}
|
precollected_items = {player: [item.code for item in world_precollected]
|
||||||
for item in world.precollected_items:
|
for player, world_precollected in world.precollected_items.items()}
|
||||||
precollected_items[item.player].append(item.code)
|
|
||||||
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
||||||
# for now special case Factorio tech_tree_information
|
# for now special case Factorio tech_tree_information
|
||||||
sending_visible_players = set()
|
sending_visible_players = set()
|
||||||
for player in world.get_game_players("Factorio"):
|
|
||||||
if world.tech_tree_information[player].value == 2:
|
|
||||||
sending_visible_players.add(player)
|
|
||||||
|
|
||||||
for slot in world.player_ids:
|
for slot in world.player_ids:
|
||||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||||
|
if world.worlds[slot].sending_visible:
|
||||||
|
sending_visible_players.add(slot)
|
||||||
|
|
||||||
def precollect_hint(location):
|
def precollect_hint(location):
|
||||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||||
|
@ -271,7 +276,7 @@ def main(args, seed=None):
|
||||||
# item code None should be event, location.address should then also be None
|
# item code None should be event, location.address should then also be None
|
||||||
assert location.item.code is not None
|
assert location.item.code is not None
|
||||||
locations_data[location.player][location.address] = location.item.code, location.item.player
|
locations_data[location.player][location.address] = location.item.code, location.item.player
|
||||||
if location.player in sending_visible_players and location.item.player != location.player:
|
if location.player in sending_visible_players:
|
||||||
precollect_hint(location)
|
precollect_hint(location)
|
||||||
elif location.name in world.start_location_hints[location.player]:
|
elif location.name in world.start_location_hints[location.player]:
|
||||||
precollect_hint(location)
|
precollect_hint(location)
|
||||||
|
@ -289,7 +294,7 @@ def main(args, seed=None):
|
||||||
world.worlds[player].remote_start_inventory},
|
world.worlds[player].remote_start_inventory},
|
||||||
"locations": locations_data,
|
"locations": locations_data,
|
||||||
"checks_in_area": checks_in_area,
|
"checks_in_area": checks_in_area,
|
||||||
"server_options": get_options()["server_options"],
|
"server_options": baked_server_options,
|
||||||
"er_hint_data": er_hint_data,
|
"er_hint_data": er_hint_data,
|
||||||
"precollected_items": precollected_items,
|
"precollected_items": precollected_items,
|
||||||
"precollected_hints": precollected_hints,
|
"precollected_hints": precollected_hints,
|
||||||
|
@ -398,9 +403,9 @@ def create_playthrough(world):
|
||||||
|
|
||||||
# second phase, sphere 0
|
# second phase, sphere 0
|
||||||
removed_precollected = []
|
removed_precollected = []
|
||||||
for item in (i for i in world.precollected_items if i.advancement):
|
for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement):
|
||||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||||
world.precollected_items.remove(item)
|
world.precollected_items[item.player].remove(item)
|
||||||
world.state.remove(item)
|
world.state.remove(item)
|
||||||
if not world.can_beat_game():
|
if not world.can_beat_game():
|
||||||
world.push_precollected(item)
|
world.push_precollected(item)
|
||||||
|
@ -464,7 +469,9 @@ def create_playthrough(world):
|
||||||
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
||||||
|
|
||||||
# we can finally output our playthrough
|
# we can finally output our playthrough
|
||||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
|
world.spoiler.playthrough = {"0": sorted([str(item) for item in
|
||||||
|
chain.from_iterable(world.precollected_items.values())
|
||||||
|
if item.advancement])}
|
||||||
|
|
||||||
for i, sphere in enumerate(collection_spheres):
|
for i, sphere in enumerate(collection_spheres):
|
||||||
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}
|
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}
|
||||||
|
|
|
@ -26,6 +26,7 @@ from prompt_toolkit.patch_stdout import patch_stdout
|
||||||
from fuzzywuzzy import process as fuzzy_process
|
from fuzzywuzzy import process as fuzzy_process
|
||||||
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
|
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
|
||||||
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||||
import Utils
|
import Utils
|
||||||
|
@ -293,7 +294,7 @@ class Context:
|
||||||
if not self.save_filename:
|
if not self.save_filename:
|
||||||
import os
|
import os
|
||||||
name, ext = os.path.splitext(self.data_filename)
|
name, ext = os.path.splitext(self.data_filename)
|
||||||
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago','.zip') \
|
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago', '.zip') \
|
||||||
else self.data_filename + '_' + 'apsave'
|
else self.data_filename + '_' + 'apsave'
|
||||||
try:
|
try:
|
||||||
with open(self.save_filename, 'rb') as f:
|
with open(self.save_filename, 'rb') as f:
|
||||||
|
@ -472,10 +473,7 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||||
# TODO ~0.2.0 remove forfeit_mode and remaining_mode in favor of permissions
|
# TODO ~0.2.0 remove forfeit_mode and remaining_mode in favor of permissions
|
||||||
'forfeit_mode': ctx.forfeit_mode,
|
'forfeit_mode': ctx.forfeit_mode,
|
||||||
'remaining_mode': ctx.remaining_mode,
|
'remaining_mode': ctx.remaining_mode,
|
||||||
'permissions': {
|
'permissions': get_permissions(ctx),
|
||||||
"forfeit": Permission.from_text(ctx.forfeit_mode),
|
|
||||||
"remaining": Permission.from_text(ctx.remaining_mode),
|
|
||||||
},
|
|
||||||
'hint_cost': ctx.hint_cost,
|
'hint_cost': ctx.hint_cost,
|
||||||
'location_check_points': ctx.location_check_points,
|
'location_check_points': ctx.location_check_points,
|
||||||
'datapackage_version': network_data_package["version"],
|
'datapackage_version': network_data_package["version"],
|
||||||
|
@ -485,6 +483,13 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||||
}])
|
}])
|
||||||
|
|
||||||
|
|
||||||
|
def get_permissions(ctx) -> typing.Dict[str, Permission]:
|
||||||
|
return {
|
||||||
|
"forfeit": Permission.from_text(ctx.forfeit_mode),
|
||||||
|
"remaining": Permission.from_text(ctx.remaining_mode),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def on_client_disconnected(ctx: Context, client: Client):
|
async def on_client_disconnected(ctx: Context, client: Client):
|
||||||
if client.auth:
|
if client.auth:
|
||||||
await on_client_left(ctx, client)
|
await on_client_left(ctx, client)
|
||||||
|
@ -972,14 +977,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
self.output("Cheating is disabled.")
|
self.output("Cheating is disabled.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@mark_raw
|
def get_hints(self, input_text: str, explicit_location: bool = False) -> bool:
|
||||||
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
|
||||||
"""Use !hint {item_name/location_name},
|
|
||||||
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
|
|
||||||
If hint costs are on, this will only give you one new result,
|
|
||||||
you can rerun the command to get more in that case."""
|
|
||||||
points_available = get_client_points(self.ctx, self.client)
|
points_available = get_client_points(self.ctx, self.client)
|
||||||
if not item_or_location:
|
if not input_text:
|
||||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||||
self.ctx.hints[self.client.team, self.client.slot]}
|
self.ctx.hints[self.client.team, self.client.slot]}
|
||||||
self.ctx.hints[self.client.team, self.client.slot] = hints
|
self.ctx.hints[self.client.team, self.client.slot] = hints
|
||||||
|
@ -989,16 +989,16 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
||||||
item_name, usable, response = get_intended_text(item_or_location, world.all_names)
|
item_name, usable, response = get_intended_text(input_text, world.all_names if not explicit_location else world.location_names)
|
||||||
if usable:
|
if usable:
|
||||||
if item_name in world.hint_blacklist:
|
if item_name in world.hint_blacklist:
|
||||||
self.output(f"Sorry, \"{item_name}\" is marked as non-hintable.")
|
self.output(f"Sorry, \"{item_name}\" is marked as non-hintable.")
|
||||||
hints = []
|
hints = []
|
||||||
elif item_name in world.item_name_groups:
|
elif item_name in world.item_name_groups and not explicit_location:
|
||||||
hints = []
|
hints = []
|
||||||
for item in world.item_name_groups[item_name]:
|
for item in world.item_name_groups[item_name]:
|
||||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
||||||
elif item_name in world.item_names: # item name
|
elif item_name in world.item_names and not explicit_location: # item name
|
||||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name)
|
hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name)
|
||||||
else: # location name
|
else: # location name
|
||||||
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name)
|
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name)
|
||||||
|
@ -1031,19 +1031,25 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
hints.append(hint)
|
hints.append(hint)
|
||||||
can_pay -= 1
|
can_pay -= 1
|
||||||
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
||||||
|
points_available = get_client_points(self.ctx, self.client)
|
||||||
|
|
||||||
if not hint.found:
|
if not hint.found:
|
||||||
self.ctx.hints[self.client.team, hint.finding_player].add(hint)
|
self.ctx.hints[self.client.team, hint.finding_player].add(hint)
|
||||||
self.ctx.hints[self.client.team, hint.receiving_player].add(hint)
|
self.ctx.hints[self.client.team, hint.receiving_player].add(hint)
|
||||||
|
|
||||||
if not_found_hints:
|
if not_found_hints:
|
||||||
if 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(
|
self.output(
|
||||||
"There may be more hintables, you can rerun the command to find more.")
|
"There may be more hintables, you can rerun the command to find more.")
|
||||||
else:
|
else:
|
||||||
self.output(f"You can't afford the hint. "
|
self.output(f"You can't afford the hint. "
|
||||||
f"You have {points_available} points and need at least "
|
f"You have {points_available} points and need at least "
|
||||||
f"{self.ctx.get_hint_cost(self.client.slot)}")
|
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||||
notify_hints(self.ctx, self.client.team, hints)
|
notify_hints(self.ctx, self.client.team, hints)
|
||||||
self.ctx.save()
|
self.ctx.save()
|
||||||
return True
|
return True
|
||||||
|
@ -1055,6 +1061,22 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
self.output(response)
|
self.output(response)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@mark_raw
|
||||||
|
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
||||||
|
"""Use !hint {item_name/location_name},
|
||||||
|
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
|
||||||
|
If hint costs are on, this will only give you one new result,
|
||||||
|
you can rerun the command to get more in that case."""
|
||||||
|
return self.get_hints(item_or_location)
|
||||||
|
|
||||||
|
@mark_raw
|
||||||
|
def _cmd_hint_location(self, location: str = "") -> bool:
|
||||||
|
"""Use !hint_location {location_name},
|
||||||
|
for example !hint atomic-bomb to get a spoiler peek for that location.
|
||||||
|
(In the case of factorio, or any other game where item names and location names are identical,
|
||||||
|
this command must be used explicitly.)"""
|
||||||
|
return self.get_hints(location, True)
|
||||||
|
|
||||||
|
|
||||||
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
|
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
|
||||||
return [location_id for
|
return [location_id for
|
||||||
|
@ -1181,7 +1203,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||||
locs = []
|
locs = []
|
||||||
for location in args["locations"]:
|
for location in args["locations"]:
|
||||||
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
||||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}])
|
await ctx.send_msgs(client,
|
||||||
|
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}])
|
||||||
return
|
return
|
||||||
target_item, target_player = ctx.locations[client.slot][location]
|
target_item, target_player = ctx.locations[client.slot][location]
|
||||||
locs.append(NetworkItem(target_item, location, target_player))
|
locs.append(NetworkItem(target_item, location, target_player))
|
||||||
|
@ -1407,6 +1430,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
return input_text
|
return input_text
|
||||||
setattr(self.ctx, option_name, attrtype(option))
|
setattr(self.ctx, option_name, attrtype(option))
|
||||||
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||||
|
if option_name in {"forfeit_mode", "remaining_mode"}:
|
||||||
|
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
|
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
|
||||||
|
|
|
@ -82,6 +82,12 @@ def page_not_found(err):
|
||||||
return render_template('404.html'), 404
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
|
# Start Playing Page
|
||||||
|
@app.route('/start-playing')
|
||||||
|
def start_playing():
|
||||||
|
return render_template(f"startPlaying.html")
|
||||||
|
|
||||||
|
|
||||||
# Player settings pages
|
# Player settings pages
|
||||||
@app.route('/games/<string:game>/player-settings')
|
@app.route('/games/<string:game>/player-settings')
|
||||||
def player_settings(game):
|
def player_settings(game):
|
||||||
|
@ -180,6 +186,9 @@ def favicon():
|
||||||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
||||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||||
|
|
||||||
|
@app.route('/discord')
|
||||||
|
def discord():
|
||||||
|
return redirect("https://discord.gg/archipelago")
|
||||||
|
|
||||||
from WebHostLib.customserver import run_server_process
|
from WebHostLib.customserver import run_server_process
|
||||||
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
||||||
|
|
|
@ -89,7 +89,7 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||||
options = restricted_loads(generation.options)
|
options = restricted_loads(generation.options)
|
||||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
logging.info(f"Generating {generation.id} for {len(options)} players")
|
||||||
pool.apply_async(gen_game, (options,),
|
pool.apply_async(gen_game, (options,),
|
||||||
{"race": meta["race"],
|
{"meta": meta,
|
||||||
"sid": generation.id,
|
"sid": generation.id,
|
||||||
"owner": generation.owner},
|
"owner": generation.owner},
|
||||||
handle_generation_success, handle_generation_failure)
|
handle_generation_success, handle_generation_failure)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import random
|
||||||
import json
|
import json
|
||||||
import zipfile
|
import zipfile
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
from typing import Dict, Optional as TypeOptional
|
||||||
|
|
||||||
from flask import request, flash, redirect, url_for, session, render_template
|
from flask import request, flash, redirect, url_for, session, render_template
|
||||||
|
|
||||||
|
@ -33,6 +34,14 @@ def generate(race=False):
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
results, gen_options = roll_options(options)
|
results, gen_options = roll_options(options)
|
||||||
|
# get form data -> server settings
|
||||||
|
hint_cost = int(request.form.get("hint_cost", 10))
|
||||||
|
forfeit_mode = request.form.get("forfeit_mode", "goal")
|
||||||
|
meta = {"race": race, "hint_cost": hint_cost, "forfeit_mode": forfeit_mode}
|
||||||
|
if race:
|
||||||
|
meta["item_cheat"] = False
|
||||||
|
meta["remaining"] = False
|
||||||
|
|
||||||
if any(type(result) == str for result in results.values()):
|
if any(type(result) == str for result in results.values()):
|
||||||
return render_template("checkResult.html", results=results)
|
return render_template("checkResult.html", results=results)
|
||||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||||
|
@ -42,7 +51,8 @@ def generate(race=False):
|
||||||
gen = Generation(
|
gen = Generation(
|
||||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||||
# convert to json compatible
|
# convert to json compatible
|
||||||
meta=json.dumps({"race": race}), state=STATE_QUEUED,
|
meta=json.dumps(meta),
|
||||||
|
state=STATE_QUEUED,
|
||||||
owner=session["_id"])
|
owner=session["_id"])
|
||||||
commit()
|
commit()
|
||||||
|
|
||||||
|
@ -50,18 +60,24 @@ def generate(race=False):
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||||
race=race, owner=session["_id"].int)
|
meta=meta, owner=session["_id"].int)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
from .autolauncher import handle_generation_failure
|
from .autolauncher import handle_generation_failure
|
||||||
handle_generation_failure(e)
|
handle_generation_failure(e)
|
||||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": "+ str(e)))
|
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
|
||||||
|
|
||||||
return redirect(url_for("viewSeed", seed=seed_id))
|
return redirect(url_for("viewSeed", seed=seed_id))
|
||||||
|
|
||||||
return render_template("generate.html", race=race)
|
return render_template("generate.html", race=race)
|
||||||
|
|
||||||
|
|
||||||
def gen_game(gen_options, race=False, owner=None, sid=None):
|
def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None):
|
||||||
|
if not meta:
|
||||||
|
meta: Dict[str, object] = {}
|
||||||
|
|
||||||
|
meta.setdefault("hint_cost", 10)
|
||||||
|
race = meta.get("race", False)
|
||||||
|
del (meta["race"])
|
||||||
try:
|
try:
|
||||||
target = tempfile.TemporaryDirectory()
|
target = tempfile.TemporaryDirectory()
|
||||||
playercount = len(gen_options)
|
playercount = len(gen_options)
|
||||||
|
@ -95,7 +111,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||||
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
|
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
|
||||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||||
|
|
||||||
ERmain(erargs, seed)
|
ERmain(erargs, seed, baked_server_options=meta)
|
||||||
|
|
||||||
return upload_to_db(target.name, sid, owner, race)
|
return upload_to_db(target.name, sid, owner, race)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
|
@ -105,7 +121,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||||
if gen is not None:
|
if gen is not None:
|
||||||
gen.state = STATE_ERROR
|
gen.state = STATE_ERROR
|
||||||
meta = json.loads(gen.meta)
|
meta = json.loads(gen.meta)
|
||||||
meta["error"] = (e.__class__.__name__ + ": "+ str(e))
|
meta["error"] = (e.__class__.__name__ + ": " + str(e))
|
||||||
gen.meta = json.dumps(meta)
|
gen.meta = json.dumps(meta)
|
||||||
|
|
||||||
commit()
|
commit()
|
||||||
|
|
|
@ -32,7 +32,10 @@ def create():
|
||||||
dictify_range=dictify_range, default_converter=default_converter,
|
dictify_range=dictify_range, default_converter=default_converter,
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f:
|
if not os.path.isdir(os.path.join(target_folder, 'configs')):
|
||||||
|
os.mkdir(os.path.join(target_folder, 'configs'))
|
||||||
|
|
||||||
|
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
|
||||||
f.write(res)
|
f.write(res)
|
||||||
|
|
||||||
# Generate JSON files for player-settings pages
|
# Generate JSON files for player-settings pages
|
||||||
|
@ -78,5 +81,8 @@ def create():
|
||||||
|
|
||||||
player_settings["gameOptions"] = game_options
|
player_settings["gameOptions"] = game_options
|
||||||
|
|
||||||
with open(os.path.join(target_folder, game_name + ".json"), "w") as f:
|
if not os.path.isdir(os.path.join(target_folder, 'player-settings')):
|
||||||
|
os.mkdir(os.path.join(target_folder, 'player-settings'))
|
||||||
|
|
||||||
|
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
|
||||||
f.write(json.dumps(player_settings, indent=2, separators=(',', ': ')))
|
f.write(json.dumps(player_settings, indent=2, separators=(',', ': ')))
|
||||||
|
|
|
@ -61,7 +61,7 @@ const fetchSettingData = () => new Promise((resolve, reject) => {
|
||||||
try{ resolve(JSON.parse(ajax.responseText)); }
|
try{ resolve(JSON.parse(ajax.responseText)); }
|
||||||
catch(error){ reject(error); }
|
catch(error){ reject(error); }
|
||||||
};
|
};
|
||||||
ajax.open('GET', `${window.location.origin}/static/generated/${gameName}.json`, true);
|
ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true);
|
||||||
ajax.send();
|
ajax.send();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,21 @@
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#generate-game-form{
|
#generate-game-form-wrapper table td{
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#generate-form-button-row{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-input{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.interactive{
|
||||||
|
color: #ffef00;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
#start-playing-wrapper{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#start-playing{
|
||||||
|
width: 700px;
|
||||||
|
min-height: 240px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
|
@ -11,11 +11,11 @@
|
||||||
{% include 'header/oceanHeader.html' %}
|
{% include 'header/oceanHeader.html' %}
|
||||||
<div id="generate-game-wrapper">
|
<div id="generate-game-wrapper">
|
||||||
<div id="generate-game" class="grass-island">
|
<div id="generate-game" class="grass-island">
|
||||||
<h1>Upload Config{% if race %} (Race Mode){% endif %}</h1>
|
<h1>Generate Game{% if race %} (Race Mode){% endif %}</h1>
|
||||||
<p>
|
<p>
|
||||||
This page allows you to generate a game by uploading a yaml file or a zip file containing yaml files.
|
This page allows you to generate a game by uploading a config file or a zip file containing config
|
||||||
If you do not have a config (yaml) file yet, you may create one on the
|
files. If you do not have a config (.yaml) file yet, you may create one on the game's settings page,
|
||||||
<a href="/player-settings">Player Settings</a> page.
|
which you can find via the <a href="{{ url_for("games") }}">supported games list</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{% if race -%}
|
{% if race -%}
|
||||||
|
@ -23,21 +23,54 @@
|
||||||
roms will be encrypted, and single-player games will have no multidata files.
|
roms will be encrypted, and single-player games will have no multidata files.
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
If you would like to generate a race game,
|
If you would like to generate a race game,
|
||||||
<a href="{{ url_for("generate", race=True) }}">click here.</a> Race games are generated without
|
<a href="{{ url_for("generate", race=True) }}">click here.</a><br />
|
||||||
a spoiler log, the ROMs are encrypted, and single-player games will not include a multidata file.
|
Race games are generated without a spoiler log, the ROMs are encrypted, and single-player games
|
||||||
|
will not include a multidata file.
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
After generation is complete, you will have the option to download a patch file.
|
|
||||||
This patch file can be opened with the
|
|
||||||
<a href="https://github.com/ArchipelagoMW/Archipelago/releases">client</a>, which can be
|
|
||||||
used to to create a rom file. In-browser patching is planned for the future.
|
|
||||||
</p>
|
|
||||||
<div id="generate-game-form-wrapper">
|
<div id="generate-game-form-wrapper">
|
||||||
<form id="generate-game-form" method="post" enctype="multipart/form-data">
|
<form id="generate-game-form" method="post" enctype="multipart/form-data">
|
||||||
<input id="file-input" type="file" name="file">
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><label for="forfeit_mode">Forfeit Permission:</label></td>
|
||||||
|
<td>
|
||||||
|
<select name="forfeit_mode" id="forfeit_mode">
|
||||||
|
<option value="auto">Automatic on goal completion</option>
|
||||||
|
<option value="goal">Allow !forfeit after goal completion</option>
|
||||||
|
<option value="auto-enabled">Automatic on goal completion and manual !forfeit</option>
|
||||||
|
<option value="enabled">Manual !forfeit</option>
|
||||||
|
<option value="disabled">Disabled</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<label for="hint_cost"> Hint Cost:</label>
|
||||||
|
<span
|
||||||
|
class="interactive"
|
||||||
|
data-tooltip="After gathering this many checks, players can !hint <itemname>
|
||||||
|
to get the location of that hint item.">(?)
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="hint_cost" id="hint_cost">
|
||||||
|
{% for n in range(0, 110, 5) %}
|
||||||
|
<option {% if n == 10 %}selected="selected" {% endif %} value="{{ n }}">
|
||||||
|
{% if n > 100 %}Off{% else %}{{ n }}%{% endif %}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div id="generate-form-button-row">
|
||||||
|
<input id="file-input" type="file" name="file">
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<button id="generate-game-button">Upload</button>
|
<button id="generate-game-button">Upload File</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<div id="base-header-right">
|
<div id="base-header-right">
|
||||||
<a href="/games">supported games</a>
|
<a href="/games">supported games</a>
|
||||||
<a href="/tutorial">setup guides</a>
|
<a href="/tutorial">setup guides</a>
|
||||||
<a href="/uploads">start game</a>
|
<a href="/start-playing">start playing</a>
|
||||||
<a href="/faq/en">f.a.q.</a>
|
<a href="/faq/en">f.a.q.</a>
|
||||||
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,17 +15,17 @@
|
||||||
<h1>Host Game</h1>
|
<h1>Host Game</h1>
|
||||||
<p>
|
<p>
|
||||||
This page allows you to host a game which was not generated by the website. For example, if you have
|
This page allows you to host a game which was not generated by the website. For example, if you have
|
||||||
generated a doors game on your own computer, you may upload the zip file created by the generator to
|
generated a game on your own computer, you may upload the zip file created by the generator to
|
||||||
host the game here. This will also provide the tracker, and the ability for your players to download
|
host the game here. This will also provide a tracker, and the ability for your players to download
|
||||||
their patch files.
|
their patch files.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
In addition to a zip file created by the generator, you may upload a multidata file here as well.
|
In addition to the zip file created by the generator, you may upload a multidata file here as well.
|
||||||
</p>
|
</p>
|
||||||
<div id="host-game-form-wrapper">
|
<div id="host-game-form-wrapper">
|
||||||
<form id="host-game-form" method="post" enctype="multipart/form-data">
|
<form id="host-game-form" method="post" enctype="multipart/form-data">
|
||||||
<input id="file-input" type="file" name="file">
|
<input id="file-input" type="file" name="file">
|
||||||
</form>
|
</form>
|
||||||
<button id="host-game-button">Upload</button>
|
<button id="host-game-button">Upload File</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<h4>multiworld multi-game randomizer</h4>
|
<h4>multiworld multi-game randomizer</h4>
|
||||||
</div>
|
</div>
|
||||||
<div id="landing-links">
|
<div id="landing-links">
|
||||||
<a href="/uploads" id="mid-button">start<br />game</a>
|
<a href="/start-playing" id="mid-button">start<br />playing</a>
|
||||||
<a href="/games" id="far-left-button">supported<br />games</a>
|
<a href="/games" id="far-left-button">supported<br />games</a>
|
||||||
<a href="/tutorial" id="mid-left-button">setup guides</a>
|
<a href="/tutorial" id="mid-left-button">setup guides</a>
|
||||||
<a href="https://discord.gg/8Z65BR2" id="far-right-button" target="_blank">discord</a>
|
<a href="https://discord.gg/8Z65BR2" id="far-right-button" target="_blank">discord</a>
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span class="variable">{{ seeds }}</span>
|
<span class="variable">{{ seeds }}</span>
|
||||||
games were created and
|
games were generated and
|
||||||
<span class="variable">{{ rooms }}</span>
|
<span class="variable">{{ rooms }}</span>
|
||||||
were hosted in the last 7 days.
|
were hosted in the last 7 days.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
A list of all games you have generated can be found <a href="/user-content">here</a>.
|
A list of all games you have generated can be found <a href="/user-content">here</a>.
|
||||||
<br />
|
<br />
|
||||||
Advanced users can download a template file for this game
|
Advanced users can download a template file for this game
|
||||||
<a href="/static/generated/{{ game }}.yaml">here</a>.
|
<a href="/static/generated/configs/{{ game }}.yaml">here</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
{% extends 'pageWrapper.html' %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<title>Start Playing</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/startPlaying.css") }}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% include 'header/oceanHeader.html' %}
|
||||||
|
|
||||||
|
<div id="start-playing-wrapper">
|
||||||
|
<div id="start-playing" class="grass-island {% if rooms %}wider{% endif %}">
|
||||||
|
<h1>Start Playing</h1>
|
||||||
|
<p>
|
||||||
|
If you're ready to start playing but don't know where to begin, check out the
|
||||||
|
<a href="/tutorial">tutorials</a> page. It has all the resources you need to create a config file
|
||||||
|
and get started. If you already have a config file, or a zip file containing them, read on.
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
To start playing a game, you'll first need to <a href="/generate">generate a randomized game</a>.
|
||||||
|
You'll need to upload either a config file or a zip file containing one more more config files.
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
If you have already generated a game and just need to host it, this site can<br />
|
||||||
|
<a href="uploads">host a pre-generated game</a> for you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include 'islandFooter.html' %}
|
||||||
|
{% endblock %}
|
|
@ -10,8 +10,12 @@
|
||||||
<div id="games">
|
<div id="games">
|
||||||
<h1>Currently Supported Games</h1>
|
<h1>Currently Supported Games</h1>
|
||||||
{% for game, description in worlds.items() %}
|
{% for game, description in worlds.items() %}
|
||||||
<h3><a href="{{ url_for("player_settings", game=game) }}">{{ game }}</a></h3>
|
<h3><a href="{{ url_for("game_info", game=game, lang="en") }}">{{ game }}</a></h3>
|
||||||
<p>{{ description }}</p>
|
<p>
|
||||||
|
<a href="{{ url_for("player_settings", game=game) }}">Settings Page</a>
|
||||||
|
<br />
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -12,10 +12,6 @@
|
||||||
<div id="view-seed-wrapper">
|
<div id="view-seed-wrapper">
|
||||||
<div id="view-seed" class="grass-island">
|
<div id="view-seed" class="grass-island">
|
||||||
<h1>Seed Info</h1>
|
<h1>Seed Info</h1>
|
||||||
{% if not seed.multidata and not seed.spoiler %}
|
|
||||||
<p>Single Player Race Rom: No spoiler or multidata exists, parts of the rom are encrypted and rooms
|
|
||||||
cannot be created.</p>
|
|
||||||
{% endif %}
|
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -33,18 +29,6 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if seed.multidata %}
|
{% if seed.multidata %}
|
||||||
<tr>
|
|
||||||
<td>Players: </td>
|
|
||||||
<td>
|
|
||||||
<ul>
|
|
||||||
{% for patch in seed.slots|sort(attribute='player_id') %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player_id) }}">{{ patch.player_name }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Rooms: </td>
|
<td>Rooms: </td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# How do I add a game to Archipelago?
|
# 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.
|
This guide is going to try and be a broad summary of how you can do just that.
|
||||||
There are three key steps to incorporating a game into Archipelago:
|
There are two key steps to incorporating a game into Archipelago:
|
||||||
- Game Modification
|
- Game Modification
|
||||||
- Archipelago Server Integration
|
- Archipelago Server Integration
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ Sent to clients when they connect to an Archipelago server.
|
||||||
| version | NetworkVersion | Object denoting the version of Archipelago which the server is running. See [NetworkVersion](#NetworkVersion) for more details. |
|
| version | NetworkVersion | Object denoting the version of Archipelago which the server is running. See [NetworkVersion](#NetworkVersion) for more details. |
|
||||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
| 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.|
|
| password | bool | Denoted whether a password is required to join this room.|
|
||||||
| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to Permission, known names: "forfeit" and "remaining". |
|
| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit" and "remaining". |
|
||||||
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
|
| 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. ||
|
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
|
||||||
| players | list\[NetworkPlayer\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. See [NetworkPlayer](#NetworkPlayer) for more details. |
|
| players | list\[NetworkPlayer\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. See [NetworkPlayer](#NetworkPlayer) for more details. |
|
||||||
|
|
|
@ -35,7 +35,7 @@ class TestInvertedOWG(TestBase):
|
||||||
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
||||||
self.world.get_location('Agahnim 1', 1).item = None
|
self.world.get_location('Agahnim 1', 1).item = None
|
||||||
self.world.get_location('Agahnim 2', 1).item = None
|
self.world.get_location('Agahnim 2', 1).item = None
|
||||||
self.world.precollected_items.clear()
|
self.world.precollected_items[1].clear()
|
||||||
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
|
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
|
||||||
mark_light_world_regions(self.world, 1)
|
mark_light_world_regions(self.world, 1)
|
||||||
self.world.worlds[1].set_rules()
|
self.world.worlds[1].set_rules()
|
||||||
|
|
|
@ -34,7 +34,7 @@ class TestVanillaOWG(TestBase):
|
||||||
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
||||||
self.world.get_location('Agahnim 1', 1).item = None
|
self.world.get_location('Agahnim 1', 1).item = None
|
||||||
self.world.get_location('Agahnim 2', 1).item = None
|
self.world.get_location('Agahnim 2', 1).item = None
|
||||||
self.world.precollected_items.clear()
|
self.world.precollected_items[1].clear()
|
||||||
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
|
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
|
||||||
mark_dark_world_regions(self.world, 1)
|
mark_dark_world_regions(self.world, 1)
|
||||||
self.world.worlds[1].set_rules()
|
self.world.worlds[1].set_rules()
|
|
@ -114,6 +114,9 @@ class World(metaclass=AutoWorldRegister):
|
||||||
item_names: Set[str] # set of all potential item names
|
item_names: Set[str] # set of all potential item names
|
||||||
location_names: Set[str] # set of all potential location names
|
location_names: Set[str] # set of all potential location names
|
||||||
|
|
||||||
|
# If there is visibility in what is being sent, this is where it will be known.
|
||||||
|
sending_visible: bool = False
|
||||||
|
|
||||||
def __init__(self, world: MultiWorld, player: int):
|
def __init__(self, world: MultiWorld, player: int):
|
||||||
self.world = world
|
self.world = world
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
|
@ -1315,9 +1315,7 @@ def patch_rom(world, rom, player, enemized):
|
||||||
equip[0x37B] = 1
|
equip[0x37B] = 1
|
||||||
equip[0x36E] = 0x80
|
equip[0x36E] = 0x80
|
||||||
|
|
||||||
for item in world.precollected_items:
|
for item in world.precollected_items[player]:
|
||||||
if item.player != player:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
||||||
'Titans Mitts', 'Power Glove', 'Progressive Glove',
|
'Titans Mitts', 'Power Glove', 'Progressive Glove',
|
||||||
|
|
|
@ -9,7 +9,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
||||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies
|
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies
|
||||||
from .Shapes import get_shapes
|
from .Shapes import get_shapes
|
||||||
from .Mod import generate_mod
|
from .Mod import generate_mod
|
||||||
from .Options import factorio_options, Silo
|
from .Options import factorio_options, Silo, TechTreeInformation
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -66,6 +66,9 @@ class Factorio(World):
|
||||||
if map_basic_settings.get("seed", None) is None: # allow seed 0
|
if map_basic_settings.get("seed", None) is None: # allow seed 0
|
||||||
map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint
|
map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint
|
||||||
|
|
||||||
|
self.sending_visible = self.world.tech_tree_information[player] == TechTreeInformation.option_full
|
||||||
|
|
||||||
|
|
||||||
generate_output = generate_mod
|
generate_output = generate_mod
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
|
|
|
@ -36,7 +36,6 @@ location_id_offset = 67000
|
||||||
|
|
||||||
# OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory.
|
# OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory.
|
||||||
i_o_limiter = threading.Semaphore(2)
|
i_o_limiter = threading.Semaphore(2)
|
||||||
hint_data_available = threading.Event()
|
|
||||||
|
|
||||||
|
|
||||||
class OOTWorld(World):
|
class OOTWorld(World):
|
||||||
|
@ -88,6 +87,10 @@ class OOTWorld(World):
|
||||||
|
|
||||||
return super().__new__(cls)
|
return super().__new__(cls)
|
||||||
|
|
||||||
|
def __init__(self, world, player):
|
||||||
|
self.hint_data_available = threading.Event()
|
||||||
|
super(OOTWorld, self).__init__(world, player)
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
# Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly
|
# Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly
|
||||||
if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16:
|
if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16:
|
||||||
|
@ -456,9 +459,7 @@ class OOTWorld(World):
|
||||||
junk_pool = get_junk_pool(self)
|
junk_pool = get_junk_pool(self)
|
||||||
removed_items = []
|
removed_items = []
|
||||||
# Determine starting items
|
# Determine starting items
|
||||||
for item in self.world.precollected_items:
|
for item in self.world.precollected_items[self.player]:
|
||||||
if item.player != self.player:
|
|
||||||
continue
|
|
||||||
if item.name in self.remove_from_start_inventory:
|
if item.name in self.remove_from_start_inventory:
|
||||||
self.remove_from_start_inventory.remove(item.name)
|
self.remove_from_start_inventory.remove(item.name)
|
||||||
removed_items.append(item.name)
|
removed_items.append(item.name)
|
||||||
|
@ -586,14 +587,20 @@ class OOTWorld(World):
|
||||||
fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations,
|
fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations,
|
||||||
itempools['any_dungeon'], True, True)
|
itempools['any_dungeon'], True, True)
|
||||||
|
|
||||||
# If anything is overworld-only, enforce them as local and not in the remaining dungeon locations
|
# If anything is overworld-only, fill into local non-dungeon locations
|
||||||
if itempools['overworld'] or self.shuffle_fortresskeys == 'overworld':
|
if self.shuffle_fortresskeys == 'overworld':
|
||||||
from worlds.generic.Rules import forbid_items_for_player
|
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.world.itempool)
|
||||||
fortresskeys = {'Small Key (Gerudo Fortress)'} if self.shuffle_fortresskeys == 'overworld' else set()
|
itempools['overworld'].extend(fortresskeys)
|
||||||
local_overworld_items = set(map(lambda item: item.name, itempools['overworld'])).union(fortresskeys)
|
if itempools['overworld']:
|
||||||
for location in self.world.get_locations():
|
for item in itempools['overworld']:
|
||||||
if location.player != self.player or location in any_dungeon_locations:
|
self.world.itempool.remove(item)
|
||||||
forbid_items_for_player(location, local_overworld_items, self.player)
|
itempools['overworld'].sort(key=lambda item:
|
||||||
|
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
|
||||||
|
non_dungeon_locations = [loc for loc in self.get_locations() if not loc.item and loc not in any_dungeon_locations
|
||||||
|
and loc.type != 'Shop' and (loc.type != 'Song' or self.shuffle_song_items != 'song')]
|
||||||
|
self.world.random.shuffle(non_dungeon_locations)
|
||||||
|
fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations,
|
||||||
|
itempools['overworld'], True, True)
|
||||||
|
|
||||||
# Place songs
|
# Place songs
|
||||||
# 5 built-in retries because this section can fail sometimes
|
# 5 built-in retries because this section can fail sometimes
|
||||||
|
@ -697,7 +704,7 @@ class OOTWorld(World):
|
||||||
|
|
||||||
def generate_output(self, output_directory: str):
|
def generate_output(self, output_directory: str):
|
||||||
if self.hints != 'none':
|
if self.hints != 'none':
|
||||||
hint_data_available.wait()
|
self.hint_data_available.wait()
|
||||||
|
|
||||||
with i_o_limiter:
|
with i_o_limiter:
|
||||||
# Make ice traps appear as other random items
|
# Make ice traps appear as other random items
|
||||||
|
@ -776,7 +783,8 @@ class OOTWorld(World):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
hint_data_available.set()
|
for autoworld in world.get_game_worlds("Ocarina of Time"):
|
||||||
|
autoworld.hint_data_available.set()
|
||||||
|
|
||||||
def modify_multidata(self, multidata: dict):
|
def modify_multidata(self, multidata: dict):
|
||||||
for item_name in self.remove_from_start_inventory:
|
for item_name in self.remove_from_start_inventory:
|
||||||
|
|
|
@ -5,10 +5,11 @@ class ItemData(NamedTuple):
|
||||||
code: int
|
code: int
|
||||||
count: int = 1
|
count: int = 1
|
||||||
progression: bool = False
|
progression: bool = False
|
||||||
|
never_exclude: bool = False
|
||||||
|
|
||||||
# A lot of items arent normally dropped by the randomizer as they are mostly enemy drops, but they can be enabled if desired
|
# A lot of items arent normally dropped by the randomizer as they are mostly enemy drops, but they can be enabled if desired
|
||||||
item_table: Dict[str, ItemData] = {
|
item_table: Dict[str, ItemData] = {
|
||||||
'Eternal Crown': ItemData('Equipment', 1337000),
|
'Eternal Crown': ItemData('Equipment', 1337000, never_exclude=True),
|
||||||
'Security Visor': ItemData('Equipment', 1337001, 0),
|
'Security Visor': ItemData('Equipment', 1337001, 0),
|
||||||
'Engineer Goggles': ItemData('Equipment', 1337002, 0),
|
'Engineer Goggles': ItemData('Equipment', 1337002, 0),
|
||||||
'Leather Helmet': ItemData('Equipment', 1337003, 0),
|
'Leather Helmet': ItemData('Equipment', 1337003, 0),
|
||||||
|
@ -39,24 +40,24 @@ item_table: Dict[str, ItemData] = {
|
||||||
'Lab Coat': ItemData('Equipment', 1337028),
|
'Lab Coat': ItemData('Equipment', 1337028),
|
||||||
'Empress Robe': ItemData('Equipment', 1337029),
|
'Empress Robe': ItemData('Equipment', 1337029),
|
||||||
'Princess Dress': ItemData('Equipment', 1337030),
|
'Princess Dress': ItemData('Equipment', 1337030),
|
||||||
'Eternal Coat': ItemData('Equipment', 1337031),
|
'Eternal Coat': ItemData('Equipment', 1337031, never_exclude=True),
|
||||||
'Synthetic Plume': ItemData('Equipment', 1337032, 0),
|
'Synthetic Plume': ItemData('Equipment', 1337032, 0),
|
||||||
'Cheveur Plume': ItemData('Equipment', 1337033, 0),
|
'Cheveur Plume': ItemData('Equipment', 1337033, 0),
|
||||||
'Metal Wristband': ItemData('Equipment', 1337034),
|
'Metal Wristband': ItemData('Equipment', 1337034),
|
||||||
'Nymph Hairband': ItemData('Equipment', 1337035, 0),
|
'Nymph Hairband': ItemData('Equipment', 1337035, 0),
|
||||||
'Mother o\' Pearl': ItemData('Equipment', 1337036, 0),
|
'Mother o\' Pearl': ItemData('Equipment', 1337036, 0),
|
||||||
'Bird Statue': ItemData('Equipment', 1337037),
|
'Bird Statue': ItemData('Equipment', 1337037, never_exclude=True),
|
||||||
'Chaos Stole': ItemData('Equipment', 1337038, 0),
|
'Chaos Stole': ItemData('Equipment', 1337038, 0),
|
||||||
'Pendulum': ItemData('Equipment', 1337039),
|
'Pendulum': ItemData('Equipment', 1337039, never_exclude=True),
|
||||||
'Chaos Horn': ItemData('Equipment', 1337040, 0),
|
'Chaos Horn': ItemData('Equipment', 1337040, 0),
|
||||||
'Filigree Clasp': ItemData('Equipment', 1337041),
|
'Filigree Clasp': ItemData('Equipment', 1337041),
|
||||||
'Azure Stole': ItemData('Equipment', 1337042, 0),
|
'Azure Stole': ItemData('Equipment', 1337042, 0),
|
||||||
'Ancient Coin': ItemData('Equipment', 1337043),
|
'Ancient Coin': ItemData('Equipment', 1337043),
|
||||||
'Shiny Rock': ItemData('Equipment', 1337044, 0),
|
'Shiny Rock': ItemData('Equipment', 1337044, 0),
|
||||||
'Galaxy Earrings': ItemData('Equipment', 1337045),
|
'Galaxy Earrings': ItemData('Equipment', 1337045, never_exclude=True),
|
||||||
'Selen\'s Bangle': ItemData('Equipment', 1337046),
|
'Selen\'s Bangle': ItemData('Equipment', 1337046, never_exclude=True),
|
||||||
'Glass Pumpkin': ItemData('Equipment', 1337047),
|
'Glass Pumpkin': ItemData('Equipment', 1337047, never_exclude=True),
|
||||||
'Gilded Egg': ItemData('Equipment', 1337048),
|
'Gilded Egg': ItemData('Equipment', 1337048, never_exclude=True),
|
||||||
'Meyef': ItemData('Familiar', 1337049),
|
'Meyef': ItemData('Familiar', 1337049),
|
||||||
'Griffin': ItemData('Familiar', 1337050),
|
'Griffin': ItemData('Familiar', 1337050),
|
||||||
'Merchant Crow': ItemData('Familiar', 1337051),
|
'Merchant Crow': ItemData('Familiar', 1337051),
|
||||||
|
@ -134,7 +135,7 @@ item_table: Dict[str, ItemData] = {
|
||||||
'Library Keycard V': ItemData('Relic', 1337123, progression=True),
|
'Library Keycard V': ItemData('Relic', 1337123, progression=True),
|
||||||
'Tablet': ItemData('Relic', 1337124, progression=True),
|
'Tablet': ItemData('Relic', 1337124, progression=True),
|
||||||
'Elevator Keycard': ItemData('Relic', 1337125, progression=True),
|
'Elevator Keycard': ItemData('Relic', 1337125, progression=True),
|
||||||
'Jewelry Box': ItemData('Relic', 1337126),
|
'Jewelry Box': ItemData('Relic', 1337126, never_exclude=True),
|
||||||
'Goddess Brooch': ItemData('Relic', 1337127),
|
'Goddess Brooch': ItemData('Relic', 1337127),
|
||||||
'Wyrm Brooch': ItemData('Relic', 1337128),
|
'Wyrm Brooch': ItemData('Relic', 1337128),
|
||||||
'Greed Brooch': ItemData('Relic', 1337129),
|
'Greed Brooch': ItemData('Relic', 1337129),
|
||||||
|
@ -171,7 +172,7 @@ item_table: Dict[str, ItemData] = {
|
||||||
'Bombardment': ItemData('Orb Spell', 1337160),
|
'Bombardment': ItemData('Orb Spell', 1337160),
|
||||||
'Corruption': ItemData('Orb Spell', 1337161),
|
'Corruption': ItemData('Orb Spell', 1337161),
|
||||||
'Lightwall': ItemData('Orb Spell', 1337162, progression=True),
|
'Lightwall': ItemData('Orb Spell', 1337162, progression=True),
|
||||||
'Bleak Ring': ItemData('Orb Passive', 1337163),
|
'Bleak Ring': ItemData('Orb Passive', 1337163, never_exclude=True),
|
||||||
'Scythe Ring': ItemData('Orb Passive', 1337164),
|
'Scythe Ring': ItemData('Orb Passive', 1337164),
|
||||||
'Pyro Ring': ItemData('Orb Passive', 1337165, progression=True),
|
'Pyro Ring': ItemData('Orb Passive', 1337165, progression=True),
|
||||||
'Royal Ring': ItemData('Orb Passive', 1337166, progression=True),
|
'Royal Ring': ItemData('Orb Passive', 1337166, progression=True),
|
||||||
|
@ -180,12 +181,12 @@ item_table: Dict[str, ItemData] = {
|
||||||
'Tailwind Ring': ItemData('Orb Passive', 1337169),
|
'Tailwind Ring': ItemData('Orb Passive', 1337169),
|
||||||
'Economizer Ring': ItemData('Orb Passive', 1337170),
|
'Economizer Ring': ItemData('Orb Passive', 1337170),
|
||||||
'Dusk Ring': ItemData('Orb Passive', 1337171),
|
'Dusk Ring': ItemData('Orb Passive', 1337171),
|
||||||
'Star of Lachiem': ItemData('Orb Passive', 1337172),
|
'Star of Lachiem': ItemData('Orb Passive', 1337172, never_exclude=True),
|
||||||
'Oculus Ring': ItemData('Orb Passive', 1337173, progression=True),
|
'Oculus Ring': ItemData('Orb Passive', 1337173, progression=True),
|
||||||
'Sanguine Ring': ItemData('Orb Passive', 1337174),
|
'Sanguine Ring': ItemData('Orb Passive', 1337174),
|
||||||
'Sun Ring': ItemData('Orb Passive', 1337175),
|
'Sun Ring': ItemData('Orb Passive', 1337175),
|
||||||
'Silence Ring': ItemData('Orb Passive', 1337176),
|
'Silence Ring': ItemData('Orb Passive', 1337176),
|
||||||
'Shadow Seal': ItemData('Orb Passive', 1337177),
|
'Shadow Seal': ItemData('Orb Passive', 1337177, never_exclude=True),
|
||||||
'Hope Ring': ItemData('Orb Passive', 1337178),
|
'Hope Ring': ItemData('Orb Passive', 1337178),
|
||||||
'Max HP': ItemData('Stat', 1337179, 12),
|
'Max HP': ItemData('Stat', 1337179, 12),
|
||||||
'Max Aura': ItemData('Stat', 1337180, 13),
|
'Max Aura': ItemData('Stat', 1337180, 13),
|
||||||
|
|
|
@ -10,7 +10,7 @@ class LocationData(NamedTuple):
|
||||||
code: Optional[int]
|
code: Optional[int]
|
||||||
rule: Callable = lambda state: True
|
rule: Callable = lambda state: True
|
||||||
|
|
||||||
def get_locations(world: Optional[MultiWorld], player: Optional[int]):
|
def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]:
|
||||||
location_table: Tuple[LocationData, ...] = (
|
location_table: Tuple[LocationData, ...] = (
|
||||||
# PresentItemLocations
|
# PresentItemLocations
|
||||||
LocationData('Tutorial', 'Yo Momma 1', 1337000),
|
LocationData('Tutorial', 'Yo Momma 1', 1337000),
|
||||||
|
|
|
@ -150,9 +150,9 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||||
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment")
|
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment")
|
||||||
|
|
||||||
|
|
||||||
def create_location(player: int, name: str, id: Optional[int], region: Region, rule: Callable, location_cache: List[Location]) -> Location:
|
def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location:
|
||||||
location = Location(player, name, id, region)
|
location = Location(player, location_data.name, location_data.code, region)
|
||||||
location.access_rule = rule
|
location.access_rule = location_data.rule
|
||||||
|
|
||||||
if id is None:
|
if id is None:
|
||||||
location.event = True
|
location.event = True
|
||||||
|
@ -169,7 +169,7 @@ def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str
|
||||||
|
|
||||||
if name in locations_per_region:
|
if name in locations_per_region:
|
||||||
for location_data in locations_per_region[name]:
|
for location_data in locations_per_region[name]:
|
||||||
location = create_location(player, location_data.name, location_data.code, region, location_data.rule, location_cache)
|
location = create_location(player, location_data, region, location_cache)
|
||||||
region.locations.append(location)
|
region.locations.append(location)
|
||||||
|
|
||||||
return region
|
return region
|
||||||
|
|
|
@ -40,11 +40,11 @@ class TimespinnerWorld(World):
|
||||||
|
|
||||||
|
|
||||||
def create_item(self, name: str) -> Item:
|
def create_item(self, name: str) -> Item:
|
||||||
return create_item(name, self.player)
|
return create_item_with_correct_settings(self.world, self.player, name)
|
||||||
|
|
||||||
|
|
||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
setup_events(self.world, self.player, self.locked_locations[self.player])
|
setup_events(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player])
|
||||||
|
|
||||||
self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player)
|
self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player)
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class TimespinnerWorld(World):
|
||||||
|
|
||||||
pool = get_item_pool(self.world, self.player, excluded_items)
|
pool = get_item_pool(self.world, self.player, excluded_items)
|
||||||
|
|
||||||
fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], pool)
|
fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player], pool)
|
||||||
|
|
||||||
self.world.itempool += pool
|
self.world.itempool += pool
|
||||||
|
|
||||||
|
@ -79,33 +79,28 @@ class TimespinnerWorld(World):
|
||||||
return slot_data
|
return slot_data
|
||||||
|
|
||||||
|
|
||||||
def create_item(name: str, player: int) -> Item:
|
def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[str]:
|
||||||
data = item_table[name]
|
excluded_items: Set[str] = set()
|
||||||
return Item(name, data.progression, data.code, player)
|
|
||||||
|
|
||||||
|
|
||||||
def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> List[str]:
|
|
||||||
excluded_items: List[str] = []
|
|
||||||
|
|
||||||
if is_option_enabled(world, player, "StartWithJewelryBox"):
|
if is_option_enabled(world, player, "StartWithJewelryBox"):
|
||||||
excluded_items.append('Jewelry Box')
|
excluded_items.add('Jewelry Box')
|
||||||
if is_option_enabled(world, player, "StartWithMeyef"):
|
if is_option_enabled(world, player, "StartWithMeyef"):
|
||||||
excluded_items.append('Meyef')
|
excluded_items.add('Meyef')
|
||||||
if is_option_enabled(world, player, "QuickSeed"):
|
if is_option_enabled(world, player, "QuickSeed"):
|
||||||
excluded_items.append('Talaria Attachment')
|
excluded_items.add('Talaria Attachment')
|
||||||
|
|
||||||
return excluded_items
|
return excluded_items
|
||||||
|
|
||||||
|
|
||||||
def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[str], locked_locations: List[str]):
|
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
||||||
melee_weapon = world.random.choice(starter_melee_weapons)
|
melee_weapon = world.random.choice(starter_melee_weapons)
|
||||||
spell = world.random.choice(starter_spells)
|
spell = world.random.choice(starter_spells)
|
||||||
|
|
||||||
excluded_items.append(melee_weapon)
|
excluded_items.add(melee_weapon)
|
||||||
excluded_items.append(spell)
|
excluded_items.add(spell)
|
||||||
|
|
||||||
melee_weapon_item = create_item(melee_weapon, player)
|
melee_weapon_item = create_item_with_correct_settings(world, player, melee_weapon)
|
||||||
spell_item = create_item(spell, player)
|
spell_item = create_item_with_correct_settings(world, player, spell)
|
||||||
|
|
||||||
world.get_location('Yo Momma 1', player).place_locked_item(melee_weapon_item)
|
world.get_location('Yo Momma 1', player).place_locked_item(melee_weapon_item)
|
||||||
world.get_location('Yo Momma 2', player).place_locked_item(spell_item)
|
world.get_location('Yo Momma 2', player).place_locked_item(spell_item)
|
||||||
|
@ -114,53 +109,57 @@ def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[st
|
||||||
locked_locations.append('Yo Momma 2')
|
locked_locations.append('Yo Momma 2')
|
||||||
|
|
||||||
|
|
||||||
def get_item_pool(world: MultiWorld, player: int, excluded_items: List[str]) -> List[Item]:
|
def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]:
|
||||||
pool: List[Item] = []
|
pool: List[Item] = []
|
||||||
|
|
||||||
for name, data in item_table.items():
|
for name, data in item_table.items():
|
||||||
if not name in excluded_items:
|
if not name in excluded_items:
|
||||||
for _ in range(data.count):
|
for _ in range(data.count):
|
||||||
item = update_progressive_state_based_flags(world, player, name, create_item(name, player))
|
item = create_item_with_correct_settings(world, player, name)
|
||||||
pool.append(item)
|
pool.append(item)
|
||||||
|
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
|
|
||||||
def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locations: List[str], pool: List[Item]):
|
def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locations: List[str],
|
||||||
for _ in range(len(get_locations(world, player)) - len(locked_locations) - len(pool)):
|
location_cache: List[Location], pool: List[Item]):
|
||||||
item = create_item(world.random.choice(filler_items), player)
|
for _ in range(len(location_cache) - len(locked_locations) - len(pool)):
|
||||||
|
item = create_item_with_correct_settings(world, player, world.random.choice(filler_items))
|
||||||
pool.append(item)
|
pool.append(item)
|
||||||
|
|
||||||
|
|
||||||
def place_first_progression_item(world: MultiWorld, player: int, excluded_items: List[str],
|
def place_first_progression_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
||||||
locked_locations: List[str]):
|
|
||||||
progression_item = world.random.choice(starter_progression_items)
|
progression_item = world.random.choice(starter_progression_items)
|
||||||
location = world.random.choice(starter_progression_locations)
|
location = world.random.choice(starter_progression_locations)
|
||||||
|
|
||||||
excluded_items.append(progression_item)
|
excluded_items.add(progression_item)
|
||||||
locked_locations.append(location)
|
locked_locations.append(location)
|
||||||
|
|
||||||
item = create_item(progression_item, player)
|
item = create_item_with_correct_settings(world, player, progression_item)
|
||||||
|
|
||||||
world.get_location(location, player).place_locked_item(item)
|
world.get_location(location, player).place_locked_item(item)
|
||||||
|
|
||||||
|
|
||||||
def update_progressive_state_based_flags(world: MultiWorld, player: int, name: str, data: Item) -> Item:
|
def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) -> Item:
|
||||||
if not data.advancement:
|
data = item_table[name]
|
||||||
return data
|
|
||||||
|
item = Item(name, data.progression, data.code, player)
|
||||||
|
item.never_exclude = data.never_exclude
|
||||||
|
|
||||||
|
if not item.advancement:
|
||||||
|
return item
|
||||||
|
|
||||||
if (name == 'Tablet' or name == 'Library Keycard V') and not is_option_enabled(world, player, "DownloadableItems"):
|
if (name == 'Tablet' or name == 'Library Keycard V') and not is_option_enabled(world, player, "DownloadableItems"):
|
||||||
data.advancement = False
|
item.advancement = False
|
||||||
if name == 'Oculus Ring' and not is_option_enabled(world, player, "FacebookMode"):
|
if name == 'Oculus Ring' and not is_option_enabled(world, player, "FacebookMode"):
|
||||||
data.advancement = False
|
item.advancement = False
|
||||||
|
|
||||||
return data
|
return item
|
||||||
|
|
||||||
|
|
||||||
def setup_events(world: MultiWorld, player: int, locked_locations: List[str]):
|
def setup_events(world: MultiWorld, player: int, locked_locations: List[str], location_cache: List[Location]):
|
||||||
for location in get_locations(world, player):
|
for location in location_cache:
|
||||||
if location.code == EventId:
|
if location.address == EventId:
|
||||||
location = world.get_location(location.name, player)
|
|
||||||
item = Item(location.name, True, EventId, player)
|
item = Item(location.name, True, EventId, player)
|
||||||
|
|
||||||
locked_locations.append(location.name)
|
locked_locations.append(location.name)
|
||||||
|
|
Loading…
Reference in New Issue