Merge branch 'main' into ror2

This commit is contained in:
alwaysintreble 2021-10-12 09:09:11 -05:00 committed by GitHub
commit f16b29b16b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 332 additions and 175 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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=(',', ': ')))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:&nbsp;</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:&nbsp;</td> <td>Rooms:&nbsp;</td>
<td> <td>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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