forked from mirror/Archipelago
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea2175cb8a | ||
|
|
11873e059a | ||
|
|
6c1023a88c | ||
|
|
0be0732a2b | ||
|
|
c9aa283711 | ||
|
|
cf2204a861 | ||
|
|
dfdcad28e5 | ||
|
|
ab4324c901 | ||
|
|
1e251dcdc0 | ||
|
|
9c1f7bfea9 | ||
|
|
5393563700 | ||
|
|
28576f2b0d | ||
|
|
ba519fecd0 | ||
|
|
86fb450ecc | ||
|
|
920240cb6f | ||
|
|
53dd0d5a7d | ||
|
|
807f544b26 | ||
|
|
1d1693df62 | ||
|
|
51574959ec | ||
|
|
04f726aef2 | ||
|
|
8a4298e504 | ||
|
|
e7f8f40464 | ||
|
|
847582ff5f | ||
|
|
1a44f5cf1c | ||
|
|
032bc75070 | ||
|
|
fb47483212 | ||
|
|
d185df3972 | ||
|
|
941dcb60e5 | ||
|
|
25756831b7 | ||
|
|
9add1495d5 | ||
|
|
34dba007dc | ||
|
|
02d3eef565 | ||
|
|
c839a76fe7 | ||
|
|
29e1c3dcf4 | ||
|
|
f6616da5a9 | ||
|
|
8678e02d54 | ||
|
|
2f37bedc92 | ||
|
|
91fdfe3e17 | ||
|
|
a41b0051a6 | ||
|
|
b8abe9f980 | ||
|
|
dd3ae5ecbd | ||
|
|
e96602d31b | ||
|
|
81d953daa3 | ||
|
|
bd774a454e | ||
|
|
ca724c92ad | ||
|
|
11eebbbd32 | ||
|
|
608794cded | ||
|
|
816de5ff02 | ||
|
|
0b941e2268 | ||
|
|
57713cda50 | ||
|
|
f56cdd6ec3 | ||
|
|
773c517757 | ||
|
|
2509b7fa3f | ||
|
|
10652d23e0 | ||
|
|
f0bc3d33ac | ||
|
|
92d1ed60c6 | ||
|
|
fe2b431821 | ||
|
|
0cc83698f9 | ||
|
|
428f643b07 | ||
|
|
d4e2b75520 | ||
|
|
96cc7f79dc | ||
|
|
bdfbc7e14a | ||
|
|
94c6562f82 | ||
|
|
22fe31a141 | ||
|
|
72fa19ee1f | ||
|
|
d899e918b4 | ||
|
|
33d31c4f0f | ||
|
|
9c3c69702a | ||
|
|
dae1a3e0f9 | ||
|
|
1f1ef10cfe | ||
|
|
760af59308 | ||
|
|
3dd7e3e706 | ||
|
|
189b129dca |
@@ -1,20 +1,20 @@
|
||||
from __future__ import annotations
|
||||
from argparse import Namespace
|
||||
|
||||
import copy
|
||||
from enum import unique, IntEnum, IntFlag
|
||||
import logging
|
||||
import json
|
||||
import functools
|
||||
from collections import OrderedDict, Counter, deque
|
||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
|
||||
import typing # this can go away when Python 3.8 support is dropped
|
||||
import secrets
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import secrets
|
||||
import typing # this can go away when Python 3.8 support is dropped
|
||||
from argparse import Namespace
|
||||
from collections import OrderedDict, Counter, deque
|
||||
from enum import unique, IntEnum, IntFlag
|
||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
|
||||
|
||||
import NetUtils
|
||||
import Options
|
||||
import Utils
|
||||
import NetUtils
|
||||
|
||||
|
||||
class Group(TypedDict, total=False):
|
||||
@@ -48,6 +48,7 @@ class MultiWorld():
|
||||
precollected_items: Dict[int, List[Item]]
|
||||
state: CollectionState
|
||||
|
||||
plando_options: PlandoOptions
|
||||
accessibility: Dict[int, Options.Accessibility]
|
||||
early_items: Dict[int, Dict[str, int]]
|
||||
local_early_items: Dict[int, Dict[str, int]]
|
||||
@@ -160,6 +161,7 @@ class MultiWorld():
|
||||
self.custom_data = {}
|
||||
self.worlds = {}
|
||||
self.slot_seeds = {}
|
||||
self.plando_options = PlandoOptions.none
|
||||
|
||||
def get_all_ids(self) -> Tuple[int, ...]:
|
||||
return self.player_ids + tuple(self.groups)
|
||||
@@ -391,7 +393,12 @@ class MultiWorld():
|
||||
def get_items(self) -> List[Item]:
|
||||
return [loc.item for loc in self.get_filled_locations()] + self.itempool
|
||||
|
||||
def find_item_locations(self, item, player: int) -> List[Location]:
|
||||
def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]:
|
||||
if resolve_group_locations:
|
||||
player_groups = self.get_player_groups(player)
|
||||
return [location for location in self.get_locations() if
|
||||
location.item and location.item.name == item and location.player not in player_groups and
|
||||
(location.item.player == player or location.item.player in player_groups)]
|
||||
return [location for location in self.get_locations() if
|
||||
location.item and location.item.name == item and location.item.player == player]
|
||||
|
||||
@@ -399,7 +406,12 @@ class MultiWorld():
|
||||
return next(location for location in self.get_locations() if
|
||||
location.item and location.item.name == item and location.item.player == player)
|
||||
|
||||
def find_items_in_locations(self, items: Set[str], player: int) -> List[Location]:
|
||||
def find_items_in_locations(self, items: Set[str], player: int, resolve_group_locations: bool = False) -> List[Location]:
|
||||
if resolve_group_locations:
|
||||
player_groups = self.get_player_groups(player)
|
||||
return [location for location in self.get_locations() if
|
||||
location.item and location.item.name in items and location.player not in player_groups and
|
||||
(location.item.player == player or location.item.player in player_groups)]
|
||||
return [location for location in self.get_locations() if
|
||||
location.item and location.item.name in items and location.item.player == player]
|
||||
|
||||
@@ -1558,6 +1570,7 @@ class Spoiler():
|
||||
Utils.__version__, self.multiworld.seed))
|
||||
outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm)
|
||||
outfile.write('Players: %d\n' % self.multiworld.players)
|
||||
outfile.write(f'Plando Options: {self.multiworld.plando_options}\n')
|
||||
AutoWorld.call_stage(self.multiworld, "write_spoiler_header", outfile)
|
||||
|
||||
for player in range(1, self.multiworld.players + 1):
|
||||
@@ -1674,6 +1687,45 @@ class Tutorial(NamedTuple):
|
||||
authors: List[str]
|
||||
|
||||
|
||||
class PlandoOptions(IntFlag):
|
||||
none = 0b0000
|
||||
items = 0b0001
|
||||
connections = 0b0010
|
||||
texts = 0b0100
|
||||
bosses = 0b1000
|
||||
|
||||
@classmethod
|
||||
def from_option_string(cls, option_string: str) -> PlandoOptions:
|
||||
result = cls(0)
|
||||
for part in option_string.split(","):
|
||||
part = part.strip().lower()
|
||||
if part:
|
||||
result = cls._handle_part(part, result)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_set(cls, option_set: Set[str]) -> PlandoOptions:
|
||||
result = cls(0)
|
||||
for part in option_set:
|
||||
result = cls._handle_part(part, result)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _handle_part(cls, part: str, base: PlandoOptions) -> PlandoOptions:
|
||||
try:
|
||||
part = cls[part]
|
||||
except Exception as e:
|
||||
raise KeyError(f"{part} is not a recognized name for a plando module. "
|
||||
f"Known options: {', '.join(flag.name for flag in cls)}") from e
|
||||
else:
|
||||
return base | part
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.value:
|
||||
return ", ".join(flag.name for flag in PlandoOptions if self.value & flag.value)
|
||||
return "None"
|
||||
|
||||
|
||||
seeddigits = 20
|
||||
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ class CommonContext:
|
||||
self.hint_cost = None
|
||||
self.slot_info = {}
|
||||
self.permissions = {
|
||||
"forfeit": "disabled",
|
||||
"release": "disabled",
|
||||
"collect": "disabled",
|
||||
"remaining": "disabled",
|
||||
}
|
||||
@@ -260,7 +260,7 @@ class CommonContext:
|
||||
self.server_task = None
|
||||
self.hint_cost = None
|
||||
self.permissions = {
|
||||
"forfeit": "disabled",
|
||||
"release": "disabled",
|
||||
"collect": "disabled",
|
||||
"remaining": "disabled",
|
||||
}
|
||||
@@ -494,7 +494,7 @@ class CommonContext:
|
||||
self._messagebox.open()
|
||||
return self._messagebox
|
||||
|
||||
def _handle_connection_loss(self, msg: str) -> None:
|
||||
def handle_connection_loss(self, msg: str) -> None:
|
||||
"""Helper for logging and displaying a loss of connection. Must be called from an except block."""
|
||||
exc_info = sys.exc_info()
|
||||
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
|
||||
@@ -580,14 +580,22 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
||||
for msg in decode(data):
|
||||
await process_server_cmd(ctx, msg)
|
||||
logger.warning(f"Disconnected from multiworld server{reconnect_hint()}")
|
||||
except websockets.InvalidMessage:
|
||||
# probably encrypted
|
||||
if address.startswith("ws://"):
|
||||
await server_loop(ctx, "ws" + address[1:])
|
||||
else:
|
||||
ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage"
|
||||
f"{reconnect_hint()}")
|
||||
except ConnectionRefusedError:
|
||||
ctx._handle_connection_loss("Connection refused by the server. May not be running Archipelago on that address or port.")
|
||||
ctx.handle_connection_loss("Connection refused by the server. "
|
||||
"May not be running Archipelago on that address or port.")
|
||||
except websockets.InvalidURI:
|
||||
ctx._handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
|
||||
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
|
||||
except OSError:
|
||||
ctx._handle_connection_loss("Failed to connect to the multiworld server")
|
||||
ctx.handle_connection_loss("Failed to connect to the multiworld server")
|
||||
except Exception:
|
||||
ctx._handle_connection_loss(f"Lost connection to the multiworld server{reconnect_hint()}")
|
||||
ctx.handle_connection_loss(f"Lost connection to the multiworld server{reconnect_hint()}")
|
||||
finally:
|
||||
await ctx.connection_closed()
|
||||
if ctx.server_address and ctx.username and not ctx.disconnected_intentionally:
|
||||
@@ -813,6 +821,10 @@ if __name__ == '__main__':
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
self.game = self.slot_info[self.slot].game
|
||||
|
||||
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||
self.game = ""
|
||||
await super().disconnect(allow_autoreconnect)
|
||||
|
||||
|
||||
async def main(args):
|
||||
|
||||
69
Generate.py
69
Generate.py
@@ -2,14 +2,13 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import random
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
from typing import Set, Dict, Tuple, Callable, Any, Union
|
||||
import os
|
||||
from collections import Counter, ChainMap
|
||||
import random
|
||||
import string
|
||||
import enum
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from collections import Counter, ChainMap
|
||||
from typing import Dict, Tuple, Callable, Any, Union
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
@@ -18,52 +17,17 @@ ModuleUpdate.update()
|
||||
import Utils
|
||||
from worlds.alttp import Options as LttPOptions
|
||||
from worlds.generic import PlandoConnection
|
||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, local_path, user_path
|
||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, user_path
|
||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
from BaseClasses import seeddigits, get_seed
|
||||
from BaseClasses import seeddigits, get_seed, PlandoOptions
|
||||
import Options
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
import copy
|
||||
|
||||
|
||||
class PlandoSettings(enum.IntFlag):
|
||||
items = 0b0001
|
||||
connections = 0b0010
|
||||
texts = 0b0100
|
||||
bosses = 0b1000
|
||||
|
||||
@classmethod
|
||||
def from_option_string(cls, option_string: str) -> PlandoSettings:
|
||||
result = cls(0)
|
||||
for part in option_string.split(","):
|
||||
part = part.strip().lower()
|
||||
if part:
|
||||
result = cls._handle_part(part, result)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_set(cls, option_set: Set[str]) -> PlandoSettings:
|
||||
result = cls(0)
|
||||
for part in option_set:
|
||||
result = cls._handle_part(part, result)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _handle_part(cls, part: str, base: PlandoSettings) -> PlandoSettings:
|
||||
try:
|
||||
part = cls[part]
|
||||
except Exception as e:
|
||||
raise KeyError(f"{part} is not a recognized name for a plando module. "
|
||||
f"Known options: {', '.join(flag.name for flag in cls)}") from e
|
||||
else:
|
||||
return base | part
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.value:
|
||||
return ", ".join(flag.name for flag in PlandoSettings if self.value & flag.value)
|
||||
return "Off"
|
||||
|
||||
|
||||
def mystery_argparse():
|
||||
@@ -97,7 +61,7 @@ def mystery_argparse():
|
||||
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
||||
if not os.path.isabs(args.meta_file_path):
|
||||
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
|
||||
args.plando: PlandoSettings = PlandoSettings.from_option_string(args.plando)
|
||||
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
|
||||
return args, options
|
||||
|
||||
|
||||
@@ -170,6 +134,7 @@ def main(args=None, callback=ERmain):
|
||||
f"A mix is also permitted.")
|
||||
erargs = parse_arguments(['--multi', str(args.multi)])
|
||||
erargs.seed = seed
|
||||
erargs.plando_options = args.plando
|
||||
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
|
||||
erargs.spoiler = args.spoiler
|
||||
erargs.race = args.race
|
||||
@@ -226,7 +191,7 @@ def main(args=None, callback=ERmain):
|
||||
elif not erargs.name[player]: # if name was not specified, generate it from filename
|
||||
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
|
||||
player += 1
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
|
||||
@@ -443,7 +408,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
|
||||
return weights
|
||||
|
||||
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoSettings):
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
|
||||
if option_key in game_weights:
|
||||
try:
|
||||
if not option.supports_weighting:
|
||||
@@ -459,7 +424,7 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
|
||||
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
|
||||
|
||||
|
||||
def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses):
|
||||
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
|
||||
if "linked_options" in weights:
|
||||
weights = roll_linked_options(weights)
|
||||
|
||||
@@ -472,7 +437,7 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
|
||||
if tuplize_version(version) > version_tuple:
|
||||
raise Exception(f"Settings reports required version of generator is at least {version}, "
|
||||
f"however generator is of version {__version__}")
|
||||
required_plando_options = PlandoSettings.from_option_string(requirements.get("plando", ""))
|
||||
required_plando_options = PlandoOptions.from_option_string(requirements.get("plando", ""))
|
||||
if required_plando_options not in plando_options:
|
||||
if required_plando_options:
|
||||
raise Exception(f"Settings reports required plando module {str(required_plando_options)}, "
|
||||
@@ -506,12 +471,12 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
|
||||
if option_key not in world_type.option_definitions and \
|
||||
(option_key not in Options.common_options or option_key in game_weights):
|
||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||
if PlandoSettings.items in plando_options:
|
||||
if PlandoOptions.items in plando_options:
|
||||
ret.plando_items = game_weights.get("plando_items", [])
|
||||
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
|
||||
# bad hardcoded behavior to make this work for now
|
||||
ret.plando_connections = []
|
||||
if PlandoSettings.connections in plando_options:
|
||||
if PlandoOptions.connections in plando_options:
|
||||
options = game_weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
@@ -626,7 +591,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
|
||||
|
||||
ret.plando_texts = {}
|
||||
if PlandoSettings.texts in plando_options:
|
||||
if PlandoOptions.texts in plando_options:
|
||||
tt = TextTable()
|
||||
tt.removeUnwantedText()
|
||||
options = weights.get("plando_texts", [])
|
||||
@@ -638,7 +603,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
|
||||
|
||||
ret.plando_connections = []
|
||||
if PlandoSettings.connections in plando_options:
|
||||
if PlandoOptions.connections in plando_options:
|
||||
options = weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
|
||||
26
Main.py
26
Main.py
@@ -38,6 +38,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
logger = logging.getLogger()
|
||||
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))
|
||||
world.plando_options = args.plando_options
|
||||
|
||||
world.shuffle = args.shuffle.copy()
|
||||
world.logic = args.logic.copy()
|
||||
@@ -121,6 +122,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
logger.info('Creating Items.')
|
||||
AutoWorld.call_all(world, "create_items")
|
||||
|
||||
# All worlds should have finished creating all regions, locations, and entrances.
|
||||
# Recache to ensure that they are all visible for locality rules.
|
||||
world._recache()
|
||||
|
||||
logger.info('Calculating Access Rules.')
|
||||
|
||||
for player in world.player_ids:
|
||||
@@ -291,27 +296,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
|
||||
oldmancaves = []
|
||||
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
|
||||
for index, take_any in enumerate(takeanyregions):
|
||||
for region in [world.get_region(take_any, player) for player in
|
||||
world.get_game_players("A Link to the Past") if world.retro_caves[player]]:
|
||||
item = world.create_item(
|
||||
region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
||||
region.player)
|
||||
player = region.player
|
||||
location_id = SHOP_ID_START + total_shop_slots + index
|
||||
|
||||
main_entrance = region.get_connecting_entrance(is_main_entrance)
|
||||
if main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[player]["Light World"].append(location_id)
|
||||
else:
|
||||
checks_in_area[player]["Dark World"].append(location_id)
|
||||
checks_in_area[player]["Total"] += 1
|
||||
|
||||
er_hint_data[player][location_id] = main_entrance.name
|
||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||
|
||||
FillDisabledShopSlots(world)
|
||||
|
||||
def write_multidata():
|
||||
|
||||
132
MultiServer.py
132
MultiServer.py
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import copy
|
||||
import functools
|
||||
import logging
|
||||
import zlib
|
||||
@@ -22,6 +23,9 @@ import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import ssl
|
||||
|
||||
import websockets
|
||||
import colorama
|
||||
try:
|
||||
@@ -40,6 +44,28 @@ min_client_version = Version(0, 1, 6)
|
||||
print_command_compatability_threshold = Version(0, 3, 5) # Remove backwards compatibility around 0.3.7
|
||||
colorama.init()
|
||||
|
||||
|
||||
def remove_from_list(container, value):
|
||||
try:
|
||||
container.remove(value)
|
||||
except ValueError:
|
||||
pass
|
||||
return container
|
||||
|
||||
|
||||
def pop_from_container(container, value):
|
||||
try:
|
||||
container.pop(value)
|
||||
except ValueError:
|
||||
pass
|
||||
return container
|
||||
|
||||
|
||||
def update_dict(dictionary, entries):
|
||||
dictionary.update(entries)
|
||||
return dictionary
|
||||
|
||||
|
||||
# functions callable on storable data on the server by clients
|
||||
modify_functions = {
|
||||
"add": operator.add, # add together two objects, using python's "+" operator (works on strings and lists as append)
|
||||
@@ -56,6 +82,10 @@ modify_functions = {
|
||||
"and": operator.and_,
|
||||
"left_shift": operator.lshift,
|
||||
"right_shift": operator.rshift,
|
||||
# lists/dicts
|
||||
"remove": remove_from_list,
|
||||
"pop": pop_from_container,
|
||||
"update": update_dict,
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +146,7 @@ class Context:
|
||||
"location_check_points": int,
|
||||
"server_password": str,
|
||||
"password": str,
|
||||
"forfeit_mode": str,
|
||||
"release_mode": str,
|
||||
"remaining_mode": str,
|
||||
"collect_mode": str,
|
||||
"item_cheat": bool,
|
||||
@@ -134,11 +164,10 @@ class Context:
|
||||
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||
forced_auto_forfeits: typing.Dict[str, bool]
|
||||
non_hintable_names: typing.Dict[str, typing.Set[str]]
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
|
||||
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
|
||||
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
||||
log_network: bool = False):
|
||||
super(Context, self).__init__()
|
||||
@@ -154,7 +183,7 @@ class Context:
|
||||
self.player_names: typing.Dict[team_slot, str] = {}
|
||||
self.player_name_lookup: typing.Dict[str, team_slot] = {}
|
||||
self.connect_names = {} # names of slots clients can connect to
|
||||
self.allow_forfeits = {}
|
||||
self.allow_releases = {}
|
||||
# player location_id item_id target_player_id
|
||||
self.locations = {}
|
||||
self.host = host
|
||||
@@ -171,7 +200,7 @@ class Context:
|
||||
self.location_check_points = location_check_points
|
||||
self.hints_used = collections.defaultdict(int)
|
||||
self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set)
|
||||
self.forfeit_mode: str = forfeit_mode
|
||||
self.release_mode: str = release_mode
|
||||
self.remaining_mode: str = remaining_mode
|
||||
self.collect_mode: str = collect_mode
|
||||
self.item_cheat = item_cheat
|
||||
@@ -204,7 +233,6 @@ class Context:
|
||||
self.gamespackage = {}
|
||||
self.item_name_groups = {}
|
||||
self.all_item_and_group_names = {}
|
||||
self.forced_auto_forfeits = collections.defaultdict(lambda: False)
|
||||
self.non_hintable_names = collections.defaultdict(frozenset)
|
||||
|
||||
self._load_game_data()
|
||||
@@ -217,7 +245,6 @@ class Context:
|
||||
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
|
||||
worlds.AutoWorldRegister.world_types.items()}
|
||||
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||
self.forced_auto_forfeits[world_name] = world.forced_auto_forfeit
|
||||
self.non_hintable_names[world_name] = world.hint_blacklist
|
||||
|
||||
def _init_game_data(self):
|
||||
@@ -317,7 +344,7 @@ class Context:
|
||||
if not client.auth:
|
||||
return
|
||||
if client.version >= print_command_compatability_threshold:
|
||||
async_start(self.send_msgs(client,
|
||||
async_start(self.send_msgs(client,
|
||||
[{"cmd": "PrintJSON", "data": [{ "text": text }]} for text in texts]))
|
||||
else:
|
||||
async_start(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
|
||||
@@ -513,9 +540,10 @@ class Context:
|
||||
"group_collected": dict(self.group_collected),
|
||||
"stored_data": self.stored_data,
|
||||
"game_options": {"hint_cost": self.hint_cost, "location_check_points": self.location_check_points,
|
||||
"server_password": self.server_password, "password": self.password, "forfeit_mode":
|
||||
self.forfeit_mode, "remaining_mode": self.remaining_mode, "collect_mode":
|
||||
self.collect_mode, "item_cheat": self.item_cheat, "compatibility": self.compatibility}
|
||||
"server_password": self.server_password, "password": self.password,
|
||||
"forfeit_mode": self.release_mode, "release_mode": self.release_mode, # TODO remove forfeit_mode around 0.4
|
||||
"remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode,
|
||||
"item_cheat": self.item_cheat, "compatibility": self.compatibility}
|
||||
|
||||
}
|
||||
|
||||
@@ -546,7 +574,7 @@ class Context:
|
||||
self.location_check_points = savedata["game_options"]["location_check_points"]
|
||||
self.server_password = savedata["game_options"]["server_password"]
|
||||
self.password = savedata["game_options"]["password"]
|
||||
self.forfeit_mode = savedata["game_options"]["forfeit_mode"]
|
||||
self.release_mode = savedata["game_options"].get("release_mode", savedata["game_options"].get("forfeit_mode", "goal"))
|
||||
self.remaining_mode = savedata["game_options"]["remaining_mode"]
|
||||
self.collect_mode = savedata["game_options"]["collect_mode"]
|
||||
self.item_cheat = savedata["game_options"]["item_cheat"]
|
||||
@@ -591,6 +619,8 @@ class Context:
|
||||
|
||||
def _set_options(self, server_options: dict):
|
||||
for key, value in server_options.items():
|
||||
if key == "forfeit_mode":
|
||||
key = "release_mode"
|
||||
data_type = self.simple_options.get(key, None)
|
||||
if data_type is not None:
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
@@ -658,10 +688,8 @@ class Context:
|
||||
self.notify_all(finished_msg)
|
||||
if "auto" in self.collect_mode:
|
||||
collect_player(self, client.team, client.slot)
|
||||
if "auto" in self.forfeit_mode:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
elif self.forced_auto_forfeits[self.games[client.slot]]:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
if "auto" in self.release_mode:
|
||||
release_player(self, client.team, client.slot)
|
||||
self.save() # save goal completion flag
|
||||
|
||||
def on_new_hint(self, team: int, slot: int):
|
||||
@@ -734,7 +762,8 @@ 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),
|
||||
"forfeit": Permission.from_text(ctx.release_mode), # TODO remove around 0.4
|
||||
"release": Permission.from_text(ctx.release_mode),
|
||||
"remaining": Permission.from_text(ctx.remaining_mode),
|
||||
"collect": Permission.from_text(ctx.collect_mode)
|
||||
}
|
||||
@@ -862,7 +891,7 @@ def update_checked_locations(ctx: Context, team: int, slot: int):
|
||||
[{"cmd": "RoomUpdate", "checked_locations": get_checked_checks(ctx, team, slot)}])
|
||||
|
||||
|
||||
def forfeit_player(ctx: Context, team: int, slot: int):
|
||||
def release_player(ctx: Context, team: int, slot: int):
|
||||
"""register any locations that are in the multidata"""
|
||||
all_locations = set(ctx.locations[slot])
|
||||
ctx.notify_all("%s (Team #%d) has released all remaining items from their world." % (ctx.player_names[(team, slot)], team + 1))
|
||||
@@ -1228,23 +1257,19 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
|
||||
def _cmd_release(self) -> bool:
|
||||
"""Sends remaining items in your world to their recipients."""
|
||||
return self._cmd_forfeit()
|
||||
|
||||
def _cmd_forfeit(self) -> bool:
|
||||
"""Surrender and send your remaining items out to their recipients. Use release in the future."""
|
||||
if self.ctx.allow_forfeits.get((self.client.team, self.client.slot), False):
|
||||
forfeit_player(self.ctx, self.client.team, self.client.slot)
|
||||
if self.ctx.allow_releases.get((self.client.team, self.client.slot), False):
|
||||
release_player(self.ctx, self.client.team, self.client.slot)
|
||||
return True
|
||||
if "enabled" in self.ctx.forfeit_mode:
|
||||
forfeit_player(self.ctx, self.client.team, self.client.slot)
|
||||
if "enabled" in self.ctx.release_mode:
|
||||
release_player(self.ctx, self.client.team, self.client.slot)
|
||||
return True
|
||||
elif "disabled" in self.ctx.forfeit_mode:
|
||||
elif "disabled" in self.ctx.release_mode:
|
||||
self.output("Sorry, client item releasing has been disabled on this server. "
|
||||
"You can ask the server admin for a /release")
|
||||
return False
|
||||
else: # is auto or goal
|
||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||
forfeit_player(self.ctx, self.client.team, self.client.slot)
|
||||
release_player(self.ctx, self.client.team, self.client.slot)
|
||||
return True
|
||||
else:
|
||||
self.output(
|
||||
@@ -1741,7 +1766,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
return
|
||||
args["cmd"] = "SetReply"
|
||||
value = ctx.stored_data.get(args["key"], args.get("default", 0))
|
||||
args["original_value"] = value
|
||||
args["original_value"] = copy.copy(value)
|
||||
for operation in args["operations"]:
|
||||
func = modify_functions[operation["operation"]]
|
||||
value = func(value, operation["value"])
|
||||
@@ -1872,28 +1897,23 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
|
||||
@mark_raw
|
||||
def _cmd_release(self, player_name: str) -> bool:
|
||||
"""Send out the remaining items from a player to their intended recipients."""
|
||||
return self._cmd_forfeit(player_name)
|
||||
|
||||
@mark_raw
|
||||
def _cmd_forfeit(self, player_name: str) -> bool:
|
||||
"""Send out the remaining items from a player to their intended recipients."""
|
||||
player = self.resolve_player(player_name)
|
||||
if player:
|
||||
team, slot, _ = player
|
||||
forfeit_player(self.ctx, team, slot)
|
||||
release_player(self.ctx, team, slot)
|
||||
return True
|
||||
|
||||
self.output(f"Could not find player {player_name} to release")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_allow_forfeit(self, player_name: str) -> bool:
|
||||
def _cmd_allow_release(self, player_name: str) -> bool:
|
||||
"""Allow the specified player to use the !release command."""
|
||||
player = self.resolve_player(player_name)
|
||||
if player:
|
||||
team, slot, name = player
|
||||
self.ctx.allow_forfeits[(team, slot)] = True
|
||||
self.ctx.allow_releases[(team, slot)] = True
|
||||
self.output(f"Player {name} is now allowed to use the !release command at any time.")
|
||||
return True
|
||||
|
||||
@@ -1901,12 +1921,12 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_forbid_forfeit(self, player_name: str) -> bool:
|
||||
def _cmd_forbid_release(self, player_name: str) -> bool:
|
||||
""""Disallow the specified player from using the !release command."""
|
||||
player = self.resolve_player(player_name)
|
||||
if player:
|
||||
team, slot, name = player
|
||||
self.ctx.allow_forfeits[(team, slot)] = False
|
||||
self.ctx.allow_releases[(team, slot)] = False
|
||||
self.output(f"Player {name} has to follow the server restrictions on use of the !release command.")
|
||||
return True
|
||||
|
||||
@@ -2061,7 +2081,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
return input_text
|
||||
setattr(self.ctx, option_name, attrtype(option))
|
||||
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||
if option_name in {"forfeit_mode", "remaining_mode", "collect_mode"}:
|
||||
if option_name in {"release_mode", "remaining_mode", "collect_mode"}:
|
||||
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
|
||||
elif option_name in {"hint_cost", "location_check_points"}:
|
||||
self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}])
|
||||
@@ -2101,19 +2121,21 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument('--password', default=defaults["password"])
|
||||
parser.add_argument('--savefile', default=defaults["savefile"])
|
||||
parser.add_argument('--disable_save', default=defaults["disable_save"], action='store_true')
|
||||
parser.add_argument('--cert', help="Path to a SSL Certificate for encryption.")
|
||||
parser.add_argument('--cert_key', help="Path to SSL Certificate Key file")
|
||||
parser.add_argument('--loglevel', default=defaults["loglevel"],
|
||||
choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int)
|
||||
parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int)
|
||||
parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true')
|
||||
parser.add_argument('--forfeit_mode', default=defaults["forfeit_mode"], nargs='?',
|
||||
parser.add_argument('--release_mode', default=defaults["release_mode"], nargs='?',
|
||||
choices=['auto', 'enabled', 'disabled', "goal", "auto-enabled"], help='''\
|
||||
Select !forfeit Accessibility. (default: %(default)s)
|
||||
auto: Automatic "forfeit" on goal completion
|
||||
enabled: !forfeit is always available
|
||||
disabled: !forfeit is never available
|
||||
goal: !forfeit can be used after goal completion
|
||||
auto-enabled: !forfeit is available and automatically triggered on goal completion
|
||||
Select !release Accessibility. (default: %(default)s)
|
||||
auto: Automatic "release" on goal completion
|
||||
enabled: !release is always available
|
||||
disabled: !release is never available
|
||||
goal: !release can be used after goal completion
|
||||
auto-enabled: !release is available and automatically triggered on goal completion
|
||||
''')
|
||||
parser.add_argument('--collect_mode', default=defaults["collect_mode"], nargs='?',
|
||||
choices=['auto', 'enabled', 'disabled', "goal", "auto-enabled"], help='''\
|
||||
@@ -2135,7 +2157,7 @@ def parse_args() -> argparse.Namespace:
|
||||
help="automatically shut down the server after this many minutes without new location checks. "
|
||||
"0 to keep running. Not yet implemented.")
|
||||
parser.add_argument('--use_embedded_options', action="store_true",
|
||||
help='retrieve forfeit, remaining and hint options from the multidata file,'
|
||||
help='retrieve release, remaining and hint options from the multidata file,'
|
||||
' instead of host.yaml')
|
||||
parser.add_argument('--compatibility', default=defaults["compatibility"], type=int,
|
||||
help="""
|
||||
@@ -2173,11 +2195,19 @@ async def auto_shutdown(ctx, to_cancel=None):
|
||||
await asyncio.sleep(seconds)
|
||||
|
||||
|
||||
def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLContext":
|
||||
import ssl
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ssl_context.load_default_certs()
|
||||
ssl_context.load_cert_chain(path, cert_key if cert_key else path)
|
||||
return ssl_context
|
||||
|
||||
|
||||
async def main(args: argparse.Namespace):
|
||||
Utils.init_logging("Server", loglevel=args.loglevel.lower())
|
||||
|
||||
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
||||
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.collect_mode,
|
||||
args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode,
|
||||
args.remaining_mode,
|
||||
args.auto_shutdown, args.compatibility, args.log_network)
|
||||
data_filename = args.multidata
|
||||
@@ -2208,8 +2238,10 @@ async def main(args: argparse.Namespace):
|
||||
|
||||
ctx.init_save(not args.disable_save)
|
||||
|
||||
ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None
|
||||
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ping_timeout=None,
|
||||
ping_interval=None)
|
||||
ping_interval=None, ssl=ssl_context)
|
||||
ip = args.host if args.host else Utils.get_public_ipv4()
|
||||
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
|
||||
'No password' if not ctx.password else 'Password: %s' % ctx.password))
|
||||
|
||||
@@ -43,7 +43,7 @@ class Permission(enum.IntFlag):
|
||||
disabled = 0b000 # 0, completely disables access
|
||||
enabled = 0b001 # 1, allows manual use
|
||||
goal = 0b010 # 2, allows manual use after goal completion
|
||||
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
|
||||
auto = 0b110 # 6, forces use after goal completion, only works for release
|
||||
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
|
||||
|
||||
@staticmethod
|
||||
@@ -86,7 +86,7 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
|
||||
data = obj._asdict()
|
||||
data["class"] = obj.__class__.__name__
|
||||
return data
|
||||
if isinstance(obj, (tuple, list, set)):
|
||||
if isinstance(obj, (tuple, list, set, frozenset)):
|
||||
return tuple(_scan_for_TypedTuples(o) for o in obj)
|
||||
if isinstance(obj, dict):
|
||||
return {key: _scan_for_TypedTuples(value) for key, value in obj.items()}
|
||||
@@ -109,7 +109,7 @@ def get_any_version(data: dict) -> Version:
|
||||
return Version(int(data["major"]), int(data["minor"]), int(data["build"]))
|
||||
|
||||
|
||||
whitelist = {
|
||||
allowlist = {
|
||||
"NetworkPlayer": NetworkPlayer,
|
||||
"NetworkItem": NetworkItem,
|
||||
"NetworkSlot": NetworkSlot
|
||||
@@ -125,7 +125,7 @@ def _object_hook(o: typing.Any) -> typing.Any:
|
||||
hook = custom_hooks.get(o.get("class", None), None)
|
||||
if hook:
|
||||
return hook(o)
|
||||
cls = whitelist.get(o.get("class", None), None)
|
||||
cls = allowlist.get(o.get("class", None), None)
|
||||
if cls:
|
||||
for key in tuple(o):
|
||||
if key not in cls._fields:
|
||||
|
||||
@@ -133,10 +133,10 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
raise NotImplementedError
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from Generate import PlandoSettings
|
||||
from Generate import PlandoOptions
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
def verify(self, world: World, player_name: str, plando_options: PlandoSettings) -> None:
|
||||
def verify(self, world: World, player_name: str, plando_options: PlandoOptions) -> None:
|
||||
pass
|
||||
else:
|
||||
def verify(self, *args, **kwargs) -> None:
|
||||
@@ -578,8 +578,8 @@ class PlandoBosses(TextChoice, metaclass=BossMeta):
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
if isinstance(self.value, int):
|
||||
return
|
||||
from Generate import PlandoSettings
|
||||
if not(PlandoSettings.bosses & plando_options):
|
||||
from Generate import PlandoOptions
|
||||
if not(PlandoOptions.bosses & plando_options):
|
||||
import logging
|
||||
# plando is disabled but plando options were given so pull the option and change it to an int
|
||||
option = self.value.split(";")[-1]
|
||||
|
||||
@@ -10,6 +10,8 @@ import re
|
||||
import sys
|
||||
import typing
|
||||
import queue
|
||||
import zipfile
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
# CommonClient import first to trigger ModuleUpdater
|
||||
@@ -120,9 +122,9 @@ class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
|
||||
return False
|
||||
|
||||
def _cmd_download_data(self, force: bool = False) -> bool:
|
||||
def _cmd_download_data(self) -> bool:
|
||||
"""Download the most recent release of the necessary files for playing SC2 with
|
||||
Archipelago. force should be True or False. force=True will overwrite your files."""
|
||||
Archipelago. Will overwrite existing files."""
|
||||
if "SC2PATH" not in os.environ:
|
||||
check_game_install_path()
|
||||
|
||||
@@ -132,11 +134,11 @@ class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
else:
|
||||
current_ver = None
|
||||
|
||||
tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData', current_version=current_ver, force_download=force)
|
||||
tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData',
|
||||
current_version=current_ver, force_download=True)
|
||||
|
||||
if tempzip != '':
|
||||
try:
|
||||
import zipfile
|
||||
zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"])
|
||||
sc2_logger.info(f"Download complete. Version {version} installed.")
|
||||
with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "w") as f:
|
||||
@@ -195,12 +197,16 @@ class SC2Context(CommonContext):
|
||||
self.build_location_to_mission_mapping()
|
||||
|
||||
# Looks for the required maps and mods for SC2. Runs check_game_install_path.
|
||||
is_mod_installed_correctly()
|
||||
maps_present = is_mod_installed_correctly()
|
||||
if os.path.exists(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt"):
|
||||
with open(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt", "r") as f:
|
||||
current_ver = f.read()
|
||||
if is_mod_update_available("TheCondor07", "Starcraft2ArchipelagoData", current_ver):
|
||||
sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.")
|
||||
elif maps_present:
|
||||
sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). "
|
||||
"Run /download_data to update them.")
|
||||
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
# goes to this world
|
||||
@@ -1003,7 +1009,7 @@ def download_latest_release_zip(owner: str, repo: str, current_version: str = No
|
||||
download_url = r1.json()["assets"][0]["browser_download_url"]
|
||||
|
||||
r2 = requests.get(download_url, headers=headers)
|
||||
if r2.status_code == 200:
|
||||
if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)):
|
||||
with open(f"{repo}.zip", "wb") as fh:
|
||||
fh.write(r2.content)
|
||||
sc2_logger.info(f"Successfully downloaded {repo}.zip.")
|
||||
|
||||
6
Utils.py
6
Utils.py
@@ -38,7 +38,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.3.7"
|
||||
__version__ = "0.3.8"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -260,7 +260,7 @@ def get_default_options() -> OptionsType:
|
||||
"disable_item_cheat": False,
|
||||
"location_check_points": 1,
|
||||
"hint_cost": 10,
|
||||
"forfeit_mode": "goal",
|
||||
"release_mode": "goal",
|
||||
"collect_mode": "disabled",
|
||||
"remaining_mode": "goal",
|
||||
"auto_shutdown": 0,
|
||||
@@ -505,7 +505,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
else:
|
||||
logging.info(f"Deleted old logfile {file.path}")
|
||||
logging.debug(f"Deleted old logfile {file.path}")
|
||||
import threading
|
||||
threading.Thread(target=_cleanup, name="LogCleaner").start()
|
||||
import platform
|
||||
|
||||
@@ -29,7 +29,7 @@ if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||
def get_app():
|
||||
register()
|
||||
app = raw_app
|
||||
if os.path.exists(configpath):
|
||||
if os.path.exists(configpath) and not app.config["TESTING"]:
|
||||
import yaml
|
||||
app.config.from_file(configpath, yaml.safe_load)
|
||||
logging.info(f"Updated config from {configpath}")
|
||||
|
||||
@@ -24,6 +24,8 @@ app.jinja_env.filters['all'] = all
|
||||
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
||||
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
||||
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
|
||||
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
||||
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
||||
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
|
||||
app.config["DEBUG"] = False
|
||||
app.config["PORT"] = 80
|
||||
|
||||
@@ -177,6 +177,8 @@ class MultiworldInstance():
|
||||
with guardian_lock:
|
||||
multiworlds[self.room_id] = self
|
||||
self.ponyconfig = config["PONY"]
|
||||
self.cert = config["SELFLAUNCHCERT"]
|
||||
self.key = config["SELFLAUNCHKEY"]
|
||||
|
||||
def start(self):
|
||||
if self.process and self.process.is_alive():
|
||||
@@ -184,7 +186,8 @@ class MultiworldInstance():
|
||||
|
||||
logging.info(f"Spinning up {self.room_id}")
|
||||
process = multiprocessing.Process(group=None, target=run_server_process,
|
||||
args=(self.room_id, self.ponyconfig, get_static_server_data()),
|
||||
args=(self.room_id, self.ponyconfig, get_static_server_data(),
|
||||
self.cert, self.key),
|
||||
name="MultiHost")
|
||||
process.start()
|
||||
# bind after start to prevent thread sync issues with guardian.
|
||||
|
||||
@@ -12,7 +12,7 @@ def allowed_file(filename):
|
||||
return filename.endswith(('.txt', ".yaml", ".zip"))
|
||||
|
||||
|
||||
from Generate import roll_settings, PlandoSettings
|
||||
from Generate import roll_settings, PlandoOptions
|
||||
from Utils import parse_yamls
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
|
||||
def roll_options(options: Dict[str, Union[dict, str]],
|
||||
plando_options: Set[str] = frozenset({"bosses", "items", "connections", "texts"})) -> \
|
||||
Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
|
||||
plando_options = PlandoSettings.from_set(set(plando_options))
|
||||
plando_options = PlandoOptions.from_set(set(plando_options))
|
||||
results = {}
|
||||
rolled_results = {}
|
||||
for filename, text in options.items():
|
||||
|
||||
@@ -10,12 +10,14 @@ import random
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
|
||||
import websockets
|
||||
from pony.orm import db_session, commit, select
|
||||
from pony.orm import commit, db_session, select
|
||||
|
||||
import Utils
|
||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
|
||||
|
||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
|
||||
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
|
||||
from .models import Room, Command, db
|
||||
|
||||
@@ -66,7 +68,6 @@ class WebHostContext(Context):
|
||||
def _load_game_data(self):
|
||||
for key, value in self.static_server_data.items():
|
||||
setattr(self, key, value)
|
||||
self.forced_auto_forfeits = collections.defaultdict(lambda: False, self.forced_auto_forfeits)
|
||||
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
||||
|
||||
def listen_to_db_commands(self):
|
||||
@@ -126,7 +127,6 @@ def get_random_port():
|
||||
def get_static_server_data() -> dict:
|
||||
import worlds
|
||||
data = {
|
||||
"forced_auto_forfeits": {},
|
||||
"non_hintable_names": {},
|
||||
"gamespackage": worlds.network_data_package["games"],
|
||||
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
|
||||
@@ -134,13 +134,13 @@ def get_static_server_data() -> dict:
|
||||
}
|
||||
|
||||
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||
data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit
|
||||
data["non_hintable_names"][world_name] = world.hint_blacklist
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
|
||||
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str]):
|
||||
# establish DB connection for multidata and multisave
|
||||
db.bind(**ponyconfig)
|
||||
db.generate_mapping(check_tables=False)
|
||||
@@ -150,15 +150,15 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
|
||||
ctx = WebHostContext(static_server_data)
|
||||
ctx.load(room_id)
|
||||
ctx.init_save()
|
||||
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||
try:
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
|
||||
ping_interval=None)
|
||||
ping_interval=None, ssl=ssl_context)
|
||||
|
||||
await ctx.server
|
||||
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None,
|
||||
ping_interval=None)
|
||||
ping_interval=None, ssl=ssl_context)
|
||||
|
||||
await ctx.server
|
||||
port = 0
|
||||
|
||||
@@ -12,7 +12,7 @@ from flask import request, flash, redirect, url_for, session, render_template
|
||||
from pony.orm import commit, db_session
|
||||
|
||||
from BaseClasses import seeddigits, get_seed
|
||||
from Generate import handle_name, PlandoSettings
|
||||
from Generate import handle_name, PlandoOptions
|
||||
from Main import main as ERmain
|
||||
from Utils import __version__
|
||||
from WebHostLib import app
|
||||
@@ -33,7 +33,7 @@ def get_meta(options_source: dict) -> dict:
|
||||
|
||||
server_options = {
|
||||
"hint_cost": int(options_source.get("hint_cost", 10)),
|
||||
"forfeit_mode": options_source.get("forfeit_mode", "goal"),
|
||||
"release_mode": options_source.get("release_mode", "goal"),
|
||||
"remaining_mode": options_source.get("remaining_mode", "disabled"),
|
||||
"collect_mode": options_source.get("collect_mode", "disabled"),
|
||||
"item_cheat": bool(int(options_source.get("item_cheat", 1))),
|
||||
@@ -119,7 +119,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
||||
erargs.outputname = seedname
|
||||
erargs.outputpath = target.name
|
||||
erargs.teams = 1
|
||||
erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options",
|
||||
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
|
||||
{"bosses", "items", "connections", "texts"}))
|
||||
|
||||
name_counter = Counter()
|
||||
|
||||
@@ -69,10 +69,6 @@ def tutorial(game, file, lang):
|
||||
|
||||
@app.route('/tutorial/')
|
||||
def tutorial_landing():
|
||||
worlds = {}
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
worlds[game] = world
|
||||
return render_template("tutorialLanding.html")
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ comfortable exploiting certain glitches in the game.
|
||||
## What is a multi-world?
|
||||
|
||||
While a randomizer shuffles a game, a multi-world randomizer shuffles that game for multiple players. For example, in a
|
||||
two player multi-world, players A and B each get their own randomized version of a game, called seeds. In each player's
|
||||
two player multi-world, players A and B each get their own randomized version of a game, called a world. In each player's
|
||||
game, they may find items which belong to the other player. If player A finds an item which belongs to player B, the
|
||||
item will be sent to player B's world over the internet.
|
||||
|
||||
@@ -29,7 +29,7 @@ their game.
|
||||
|
||||
## What happens if a person has to leave early?
|
||||
|
||||
If a player must leave early, they can use Archipelago's forfeit system. When a player forfeits their game, all the
|
||||
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all the
|
||||
items in that game which belong to other players are sent out automatically, so other players can continue to play.
|
||||
|
||||
## What does multi-game mean?
|
||||
|
||||
@@ -205,6 +205,11 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
let presetOption = document.createElement('option');
|
||||
presetOption.innerText = presetName;
|
||||
presetOption.value = settings[setting].value_names[presetName];
|
||||
const words = presetOption.innerText.split("_");
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
||||
}
|
||||
presetOption.innerText = words.join(" ");
|
||||
specialRangeSelect.appendChild(presetOption);
|
||||
});
|
||||
let customOption = document.createElement('option');
|
||||
|
||||
@@ -40,20 +40,20 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="forfeit_mode">Forfeit Permission:
|
||||
<span class="interactive" data-tooltip="A forfeit releases all remaining items from the locations in your world.">
|
||||
<label for="release_mode">Release Permission:
|
||||
<span class="interactive" data-tooltip="Permissions on when players are able to release all remaining items from their world.">
|
||||
(?)
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<select name="forfeit_mode" id="forfeit_mode">
|
||||
<select name="release_mode" id="release_mode">
|
||||
<option value="auto">Automatic on goal completion</option>
|
||||
<option value="goal">Allow !forfeit after goal completion</option>
|
||||
<option value="goal">Allow !release after goal completion</option>
|
||||
<option value="auto-enabled">
|
||||
Automatic on goal completion and manual !forfeit
|
||||
Automatic on goal completion and manual !release
|
||||
</option>
|
||||
<option value="enabled">Manual !forfeit</option>
|
||||
<option value="enabled">Manual !release</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
</select>
|
||||
</td>
|
||||
@@ -62,7 +62,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<label for="collect_mode">Collect Permission:
|
||||
<span class="interactive" data-tooltip="A collect releases all of your remaining items to you from across the multiworld.">
|
||||
<span class="interactive" data-tooltip="Permissions on when players are able to collect all their remaining items from across the multiworld.">
|
||||
(?)
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
|
||||
<h2>Game Info Pages</h2>
|
||||
<ul>
|
||||
{% for game in games %}
|
||||
{% for game in games | title_sorted %}
|
||||
<li><a href="{{ url_for('game_info', game=game, lang='en') }}">{{ game }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Game Settings Pages</h2>
|
||||
<ul>
|
||||
{% for game in games %}
|
||||
{% for game in games | title_sorted %}
|
||||
<li><a href="{{ url_for('player_settings', game=game) }}">{{ game }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -650,7 +650,7 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
|
||||
|
||||
# Gather dungeon locations
|
||||
area_id_ranges = {
|
||||
"Overworld": ((67000, 67258), (67264, 67280), (67747, 68024), (68054, 68062)),
|
||||
"Overworld": ((67000, 67263), (67269, 67280), (67747, 68024), (68054, 68062)),
|
||||
"Deku Tree": ((67281, 67303), (68063, 68077)),
|
||||
"Dodongo's Cavern": ((67304, 67334), (68078, 68160)),
|
||||
"Jabu Jabu's Belly": ((67335, 67359), (68161, 68188)),
|
||||
@@ -662,7 +662,7 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
|
||||
"Spirit Temple": ((67533, 67582), (68566, 68625)),
|
||||
"Ice Cavern": ((67583, 67596), (68626, 68649)),
|
||||
"Gerudo Training Ground": ((67597, 67635), (68650, 68656)),
|
||||
"Thieves' Hideout": ((67259, 67263), (68025, 68053)),
|
||||
"Thieves' Hideout": ((67264, 67268), (68025, 68053)),
|
||||
"Ganon's Castle": ((67636, 67673), (68657, 68705)),
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ class ZillionContext(CommonContext):
|
||||
command_processor: Type[ClientCommandProcessor] = ZillionCommandProcessor
|
||||
items_handling = 1 # receive items from other players
|
||||
|
||||
known_name: Optional[str]
|
||||
""" This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """
|
||||
|
||||
from_game: "asyncio.Queue[events.EventFromGame]"
|
||||
to_game: "asyncio.Queue[events.EventToGame]"
|
||||
ap_local_count: int
|
||||
@@ -82,6 +85,7 @@ class ZillionContext(CommonContext):
|
||||
server_address: str,
|
||||
password: str) -> None:
|
||||
super().__init__(server_address, password)
|
||||
self.known_name = None
|
||||
self.from_game = asyncio.Queue()
|
||||
self.to_game = asyncio.Queue()
|
||||
self.got_room_info = asyncio.Event()
|
||||
@@ -396,7 +400,8 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
|
||||
game_id = memory.get_rom_to_ram_data(ram)
|
||||
name, seed_end = name_seed_from_ram(game_id)
|
||||
if len(name):
|
||||
if name == ctx.auth:
|
||||
if name == ctx.known_name:
|
||||
ctx.auth = name
|
||||
# this is the name we know
|
||||
if ctx.server and ctx.server.socket: # type: ignore
|
||||
if ctx.got_room_info.is_set():
|
||||
@@ -439,6 +444,7 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
|
||||
memory.reset_game_state()
|
||||
|
||||
ctx.auth = name
|
||||
ctx.known_name = name
|
||||
async_start(ctx.connect())
|
||||
await asyncio.wait((
|
||||
ctx.got_room_info.wait(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<TabbedPanel>
|
||||
tab_width: 200
|
||||
tab_width: root.width / app.tab_count
|
||||
<SelectableLabel>:
|
||||
canvas.before:
|
||||
Color:
|
||||
|
||||
@@ -148,7 +148,7 @@ The next step is to know what you need to make the game do now that you can modi
|
||||
- Listen for messages from the Archipelago server
|
||||
- Modify the game to display messages from the Archipelago server
|
||||
- Add interface for connecting to the Archipelago server with passwords and sessions
|
||||
- Add commands for manually rewarding, re-syncing, forfeiting, and other actions
|
||||
- Add commands for manually rewarding, re-syncing, releasing, and other actions
|
||||
|
||||
To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive
|
||||
from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary,
|
||||
|
||||
@@ -8,4 +8,4 @@ We conduct ourselves openly and inclusively here. Please do not contribute to an
|
||||
|
||||
These guidelines apply to all channels of communication within this GitHub repository. Please be respectful in both public channels, such as issues, and private ones, such as private messaging or emails.
|
||||
|
||||
Any incidents of abuse may be reported directly to ijwu at hmfarran@gmail.com.
|
||||
Any incidents of abuse may be reported directly to eudaimonistic at eudaimonistic42@gmail.com
|
||||
|
||||
@@ -70,7 +70,7 @@ Sent to clients when they connect to an Archipelago server.
|
||||
| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
||||
| password | bool | Denoted whether a password is required to join this room.|
|
||||
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" and "remaining". |
|
||||
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". |
|
||||
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
|
||||
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
|
||||
| games | list\[str\] | List of games present in this multiworld. |
|
||||
@@ -78,14 +78,14 @@ Sent to clients when they connect to an Archipelago server.
|
||||
| seed_name | str | uniquely identifying name of this generation |
|
||||
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
|
||||
|
||||
#### forfeit
|
||||
Dictates what is allowed when it comes to a player forfeiting their run. A forfeit is an action which distributes the rest of the items in a player's run to those other players awaiting them.
|
||||
#### release
|
||||
Dictates what is allowed when it comes to a player releasing their run. A release is an action which distributes the rest of the items in a player's run to those other players awaiting them.
|
||||
|
||||
* `auto`: Distributes a player's items to other players when they complete their goal.
|
||||
* `enabled`: Denotes that players may forfeit at any time in the game.
|
||||
* `enabled`: Denotes that players may release at any time in the game.
|
||||
* `auto-enabled`: Both of the above options together.
|
||||
* `disabled`: All forfeit modes disabled.
|
||||
* `goal`: Allows for manual use of forfeit command once a player completes their goal. (Disabled until goal completion)
|
||||
* `disabled`: All release modes disabled.
|
||||
* `goal`: Allows for manual use of release command once a player completes their goal. (Disabled until goal completion)
|
||||
|
||||
#### collect
|
||||
Dictates what is allowed when it comes to a player collecting their run. A collect is an action which sends the rest of the items in a player's run.
|
||||
@@ -411,6 +411,9 @@ The following operations can be applied to a datastorage key
|
||||
| xor | Applies a bitwise Exclusive OR to the current value of the key with `value`. |
|
||||
| left_shift | Applies a bitwise left-shift to the current value of the key by `value`. |
|
||||
| right_shift | Applies a bitwise right-shift to the current value of the key by `value`. |
|
||||
| remove | List only: removes the first instance of `value` found in the list. |
|
||||
| pop | List or Dict: for lists it will remove the index of the `value` given. for dicts it removes the element with the specified key of `value`. |
|
||||
| update | Dict only: Updates the dictionary with the specified elements given in `value` creating new keys, or updating old ones if they previously existed. |
|
||||
|
||||
### SetNotify
|
||||
Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes.
|
||||
@@ -596,7 +599,7 @@ class Permission(enum.IntEnum):
|
||||
disabled = 0b000 # 0, completely disables access
|
||||
enabled = 0b001 # 1, allows manual use
|
||||
goal = 0b010 # 2, allows manual use after goal completion
|
||||
auto = 0b110 # 6, forces use after goal completion, only works for forfeit and collect
|
||||
auto = 0b110 # 6, forces use after goal completion, only works for release and collect
|
||||
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
|
||||
```
|
||||
|
||||
|
||||
16
host.yaml
16
host.yaml
@@ -22,14 +22,14 @@ server_options:
|
||||
# Relative point cost to receive a hint via !hint for players
|
||||
# so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5
|
||||
hint_cost: 10 # Set to 0 if you want free hints
|
||||
# Forfeit modes
|
||||
# A Forfeit sends out the remaining items *from* a world that forfeits
|
||||
# "disabled" -> clients can't forfeit,
|
||||
# "enabled" -> clients can always forfeit
|
||||
# "auto" -> automatic forfeit on goal completion
|
||||
# "auto-enabled" -> automatic forfeit on goal completion and manual forfeit is also enabled
|
||||
# "goal" -> forfeit is allowed after goal completion
|
||||
forfeit_mode: "goal"
|
||||
# Release modes
|
||||
# A Release sends out the remaining items *from* a world that releases
|
||||
# "disabled" -> clients can't release,
|
||||
# "enabled" -> clients can always release
|
||||
# "auto" -> automatic release on goal completion
|
||||
# "auto-enabled" -> automatic release on goal completion and manual release is also enabled
|
||||
# "goal" -> release is allowed after goal completion
|
||||
release_mode: "goal"
|
||||
# Collect modes
|
||||
# A Collect sends the remaining items *to* a world that collects
|
||||
# "disabled" -> clients can't collect,
|
||||
|
||||
16
kvui.py
16
kvui.py
@@ -330,6 +330,12 @@ class GameManager(App):
|
||||
|
||||
super(GameManager, self).__init__()
|
||||
|
||||
@property
|
||||
def tab_count(self):
|
||||
if hasattr(self, "tabs"):
|
||||
return max(1, len(self.tabs.tab_list))
|
||||
return 1
|
||||
|
||||
def build(self) -> Layout:
|
||||
self.container = ContainerLayout()
|
||||
|
||||
@@ -392,11 +398,13 @@ class GameManager(App):
|
||||
Clock.schedule_interval(self.update_texts, 1 / 30)
|
||||
self.container.add_widget(self.grid)
|
||||
|
||||
# If the address contains a port, select it; otherwise, select the host.
|
||||
s = self.server_connect_bar.text
|
||||
host_start = s.find("@") + 1
|
||||
ipv6_end = s.find("]", host_start) + 1
|
||||
port_start = s.find(":", ipv6_end if ipv6_end > 0 else host_start) + 1
|
||||
self.server_connect_bar.focus = True
|
||||
self.server_connect_bar.select_text(
|
||||
self.server_connect_bar.text.find(":") + 1,
|
||||
len(self.server_connect_bar.text)
|
||||
)
|
||||
self.server_connect_bar.select_text(port_start if port_start > 0 else host_start, len(s))
|
||||
|
||||
return self.container
|
||||
|
||||
|
||||
@@ -27,17 +27,62 @@ game: # Pick a game to play
|
||||
A Link to the Past: 1
|
||||
requires:
|
||||
version: 0.3.3 # Version of Archipelago required for this yaml to work as expected.
|
||||
# Shared Options supported by all games:
|
||||
accessibility:
|
||||
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
|
||||
locations: 50 # Guarantees you will be able to access all locations, and therefore all items
|
||||
none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
|
||||
progression_balancing: # A system to reduce BK, as in times during which you can't do anything, by moving your items into an earlier access sphere
|
||||
0: 0 # Choose a lower number if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
|
||||
25: 0
|
||||
50: 50 # Make it likely you have stuff to do.
|
||||
99: 0 # Get important items early, and stay at the front of the progression.
|
||||
A Link to the Past:
|
||||
progression_balancing:
|
||||
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
||||
# A lower setting means more getting stuck. A higher setting means less getting stuck.
|
||||
#
|
||||
# You can define additional values between the minimum and maximum values.
|
||||
# Minimum value is 0
|
||||
# Maximum value is 99
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
disabled: 0 # equivalent to 0
|
||||
normal: 50 # equivalent to 50
|
||||
extreme: 0 # equivalent to 99
|
||||
|
||||
accessibility:
|
||||
# Set rules for reachability of your items/locations.
|
||||
# Locations: ensure everything can be reached and acquired.
|
||||
# Items: ensure all logically relevant items can be acquired.
|
||||
# Minimal: ensure what is needed to reach your goal can be acquired.
|
||||
locations: 0
|
||||
items: 50
|
||||
minimal: 0
|
||||
|
||||
local_items:
|
||||
# Forces these items to be in their native world.
|
||||
[ ]
|
||||
|
||||
non_local_items:
|
||||
# Forces these items to be outside their native world.
|
||||
[ ]
|
||||
|
||||
start_inventory:
|
||||
# Start with these items.
|
||||
{ }
|
||||
|
||||
start_hints:
|
||||
# Start with these item's locations prefilled into the !hint command.
|
||||
[ ]
|
||||
|
||||
start_location_hints:
|
||||
# Start with these locations and their item prefilled into the !hint command
|
||||
[ ]
|
||||
|
||||
exclude_locations:
|
||||
# Prevent these locations from having an important item
|
||||
[ ]
|
||||
|
||||
priority_locations:
|
||||
# Prevent these locations from having an unimportant item
|
||||
[ ]
|
||||
|
||||
item_links:
|
||||
# Share part of your item pool with other players.
|
||||
[ ]
|
||||
|
||||
### Logic Section ###
|
||||
glitches_required: # Determine the logic required to complete the seed
|
||||
none: 50 # No glitches required
|
||||
|
||||
38
setup.py
38
setup.py
@@ -11,7 +11,10 @@ from collections.abc import Iterable
|
||||
from hashlib import sha3_512
|
||||
from pathlib import Path
|
||||
|
||||
import setuptools
|
||||
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from Launcher import components, icon_paths
|
||||
from Utils import version_tuple, is_windows, is_linux
|
||||
@@ -26,7 +29,7 @@ apworlds: set = {
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
import subprocess
|
||||
import pkg_resources
|
||||
requirement = 'cx-Freeze>=6.13.1'
|
||||
requirement = 'cx-Freeze>=6.14.1'
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
import cx_Freeze
|
||||
@@ -36,12 +39,15 @@ except pkg_resources.ResolutionError:
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade'])
|
||||
import cx_Freeze
|
||||
|
||||
# .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line
|
||||
import setuptools.command.build
|
||||
|
||||
if os.path.exists("X:/pw.txt"):
|
||||
print("Using signtool")
|
||||
with open("X:/pw.txt", encoding="utf-8-sig") as f:
|
||||
pw = f.read()
|
||||
signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + r'" /fd sha256 /tr http://timestamp.digicert.com/ '
|
||||
signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \
|
||||
r'" /fd sha256 /tr http://timestamp.digicert.com/ '
|
||||
else:
|
||||
signtool = None
|
||||
|
||||
@@ -64,6 +70,7 @@ exes = [
|
||||
]
|
||||
|
||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"]
|
||||
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
|
||||
|
||||
|
||||
def remove_sprites_from_folder(folder):
|
||||
@@ -79,7 +86,7 @@ def _threaded_hash(filepath):
|
||||
|
||||
|
||||
# cx_Freeze's build command runs other commands. Override to accept --yes and store that.
|
||||
class BuildCommand(cx_Freeze.command.build.Build):
|
||||
class BuildCommand(setuptools.command.build.build):
|
||||
user_options = [
|
||||
('yes', 'y', 'Answer "yes" to all questions.'),
|
||||
]
|
||||
@@ -103,6 +110,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
]
|
||||
yes: bool
|
||||
extra_data: Iterable # [any] not available in 3.8
|
||||
extra_libs: Iterable # work around broken include_files
|
||||
|
||||
buildfolder: Path
|
||||
libfolder: Path
|
||||
@@ -113,6 +121,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
super().initialize_options()
|
||||
self.yes = BuildCommand.last_yes
|
||||
self.extra_data = []
|
||||
self.extra_libs = []
|
||||
|
||||
def finalize_options(self):
|
||||
super().finalize_options()
|
||||
@@ -169,17 +178,22 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
self.buildtime = datetime.datetime.utcnow()
|
||||
super().run()
|
||||
|
||||
# include_files seems to be broken with this setup. implement here
|
||||
# include_files seems to not be done automatically. implement here
|
||||
for src, dst in self.include_files:
|
||||
print('copying', src, '->', self.buildfolder / dst)
|
||||
print(f"copying {src} -> {self.buildfolder / dst}")
|
||||
shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False)
|
||||
|
||||
# now that include_files is completely broken, run find_libs here
|
||||
for src, dst in find_libs(*self.extra_libs):
|
||||
print(f"copying {src} -> {self.buildfolder / dst}")
|
||||
shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False)
|
||||
|
||||
# post build steps
|
||||
if sys.platform == "win32": # kivy_deps is win32 only, linux picks them up automatically
|
||||
if is_windows: # kivy_deps is win32 only, linux picks them up automatically
|
||||
from kivy_deps import sdl2, glew
|
||||
for folder in sdl2.dep_bins + glew.dep_bins:
|
||||
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
|
||||
print('copying', folder, '->', self.libfolder)
|
||||
print(f"copying {folder} -> {self.libfolder}")
|
||||
|
||||
for data in self.extra_data:
|
||||
self.installfile(Path(data))
|
||||
@@ -380,6 +394,9 @@ $APPDIR/$exe "$@"
|
||||
|
||||
def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]:
|
||||
"""Try to find system libraries to be included."""
|
||||
if not args:
|
||||
return []
|
||||
|
||||
arch = build_arch.replace('_', '-')
|
||||
libc = 'libc6' # we currently don't support musl
|
||||
|
||||
@@ -446,12 +463,13 @@ cx_Freeze.setup(
|
||||
"pandas"],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": ["worlds", "sc2"],
|
||||
"include_files": find_libs("libssl.so", "libcrypto.so") if is_linux else [],
|
||||
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||
"include_msvcr": False,
|
||||
"replace_paths": [("*", "")],
|
||||
"replace_paths": ["*."],
|
||||
"optimize": 1,
|
||||
"build_exe": buildfolder,
|
||||
"extra_data": extra_data,
|
||||
"extra_libs": extra_libs,
|
||||
"bin_includes": ["libffi.so", "libcrypt.so"] if is_linux else []
|
||||
},
|
||||
"bdist_appimage": {
|
||||
|
||||
@@ -169,6 +169,9 @@ class WorldTestBase(unittest.TestCase):
|
||||
def can_reach_location(self, location: str) -> bool:
|
||||
return self.multiworld.state.can_reach(location, "Location", 1)
|
||||
|
||||
def can_reach_entrance(self, entrance: str) -> bool:
|
||||
return self.multiworld.state.can_reach(entrance, "Entrance", 1)
|
||||
|
||||
def count(self, item_name: str) -> int:
|
||||
return self.multiworld.state.count(item_name, 1)
|
||||
|
||||
|
||||
@@ -33,6 +33,14 @@ class TestBase(unittest.TestCase):
|
||||
for item in items:
|
||||
self.assertIn(item, world_type.item_name_to_id)
|
||||
|
||||
def testItemNameGroupConflict(self):
|
||||
"""Test that all item name groups aren't also item names."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game_name, game_name=game_name):
|
||||
for group_name in world_type.item_name_groups:
|
||||
with self.subTest(group_name, group_name=group_name):
|
||||
self.assertNotIn(group_name, world_type.item_name_to_id)
|
||||
|
||||
def testItemCountGreaterEqualLocations(self):
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
|
||||
|
||||
16
test/general/TestLocations.py
Normal file
16
test/general/TestLocations.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import unittest
|
||||
from collections import Counter
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from . import setup_default_world
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def testCreateDuplicateLocations(self):
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if game_name in {"Final Fantasy"}:
|
||||
continue
|
||||
multiworld = setup_default_world(world_type)
|
||||
locations = Counter(multiworld.get_locations())
|
||||
if locations:
|
||||
self.assertLessEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location {locations.most_common(1)}")
|
||||
@@ -11,10 +11,11 @@ class TestDocs(unittest.TestCase):
|
||||
"filename": ":memory:",
|
||||
"create_db": True,
|
||||
}
|
||||
app = get_app()
|
||||
app.config.update({
|
||||
raw_app.config.update({
|
||||
"TESTING": True,
|
||||
})
|
||||
app = get_app()
|
||||
|
||||
cls.client = app.test_client()
|
||||
|
||||
def testCorrectErrorEmptyRequest(self):
|
||||
|
||||
@@ -109,10 +109,10 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
|
||||
class WebWorld:
|
||||
"""Webhost integration"""
|
||||
|
||||
|
||||
settings_page: Union[bool, str] = True
|
||||
"""display a settings page. Can be a link to a specific page or external tool."""
|
||||
|
||||
|
||||
game_info_languages: List[str] = ['en']
|
||||
"""docs folder will be scanned for game info pages using this list in the format '{language}_{game_name}.md'"""
|
||||
|
||||
@@ -160,10 +160,6 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset() # any names that should not be hintable
|
||||
|
||||
# For games where after a victory it is impossible to go back in and get additional/remaining Locations checked.
|
||||
# this forces forfeit: auto for those games.
|
||||
forced_auto_forfeit: bool = False
|
||||
|
||||
# Hide World Type from various views. Does not remove functionality.
|
||||
hidden: ClassVar[bool] = False
|
||||
|
||||
|
||||
@@ -322,7 +322,7 @@ location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
|
||||
location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()}
|
||||
|
||||
|
||||
async def track_locations(ctx, roomid, roomdata):
|
||||
async def track_locations(ctx, roomid, roomdata) -> bool:
|
||||
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
||||
new_locations = []
|
||||
|
||||
@@ -451,10 +451,126 @@ async def track_locations(ctx, roomid, roomdata):
|
||||
if misc_data_changed:
|
||||
snes_buffered_write(ctx, SAVEDATA_START + 0x3c6, bytes(misc_data))
|
||||
|
||||
|
||||
if new_locations:
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
|
||||
# verify rom is still the same:
|
||||
rom_name = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
||||
if rom_name is None or all(byte == b"\x00" for byte in rom_name) or rom_name[:2] != b"AP" or \
|
||||
rom_name != ctx.rom:
|
||||
snes_logger.info(f"Discarding recent {len(new_locations)} checks as ROM Status has changed.")
|
||||
return False
|
||||
else:
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
|
||||
await snes_flush_writes(ctx)
|
||||
return True
|
||||
|
||||
|
||||
class ALTTPSNIClient(SNIClient):
|
||||
game = "A Link to the Past"
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes
|
||||
invincible = await snes_read(ctx, WRAM_START + 0x037B, 1)
|
||||
last_health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
|
||||
await asyncio.sleep(0.25)
|
||||
health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
|
||||
if not invincible or not last_health or not health:
|
||||
ctx.death_state = DeathState.dead
|
||||
ctx.last_death_link = time.time()
|
||||
return
|
||||
if not invincible[0] and last_health[0] == health[0]:
|
||||
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
|
||||
snes_buffered_write(ctx, WRAM_START + 0x0373,
|
||||
bytes([8])) # deal 1 full heart of damage at next opportunity
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if not gamemode or gamemode[0] in DEATH_MODES:
|
||||
ctx.death_state = DeathState.dead
|
||||
|
||||
async def validate_rom(self, ctx) -> bool:
|
||||
from SNIClient import snes_read
|
||||
|
||||
rom_name = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
||||
if rom_name is None or all(byte == b"\x00" for byte in rom_name) or rom_name[:2] != b"AP":
|
||||
return False
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b001 # full local
|
||||
|
||||
ctx.rom = rom_name
|
||||
|
||||
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
|
||||
if death_link:
|
||||
ctx.allow_collect = bool(death_link[0] & 0b100)
|
||||
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
|
||||
return True
|
||||
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = gamemode[0] in DEATH_MODES
|
||||
await ctx.handle_deathlink_state(currently_dead)
|
||||
|
||||
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
|
||||
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
|
||||
if gamemode is None or gameend is None or game_timer is None or \
|
||||
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
|
||||
return
|
||||
|
||||
if gameend[0]:
|
||||
if not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
if gamemode in ENDGAME_MODES: # triforce room and credits
|
||||
return
|
||||
|
||||
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2]
|
||||
roomid = data[4] | (data[5] << 8)
|
||||
roomdata = data[6]
|
||||
scout_location = data[7]
|
||||
|
||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||
item = ctx.items_received[recv_index]
|
||||
recv_index += 1
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names[item.item], 'red', 'bold'),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
|
||||
|
||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
|
||||
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_ADDR,
|
||||
bytes([item.item]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR,
|
||||
bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0]))
|
||||
if scout_location > 0 and scout_location in ctx.locations_info:
|
||||
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR,
|
||||
bytes([scout_location]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR,
|
||||
bytes([ctx.locations_info[scout_location].item]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR,
|
||||
bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location].player)]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
||||
ctx.locations_scouted.add(scout_location)
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
|
||||
same_rom = await track_locations(ctx, roomid, roomdata)
|
||||
if not same_rom:
|
||||
return
|
||||
|
||||
|
||||
def get_alttp_settings(romfile: str):
|
||||
@@ -582,112 +698,3 @@ def get_alttp_settings(romfile: str):
|
||||
else:
|
||||
adjusted = False
|
||||
return adjustedromfile, adjusted
|
||||
|
||||
|
||||
class ALTTPSNIClient(SNIClient):
|
||||
game = "A Link to the Past"
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes
|
||||
invincible = await snes_read(ctx, WRAM_START + 0x037B, 1)
|
||||
last_health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
|
||||
await asyncio.sleep(0.25)
|
||||
health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
|
||||
if not invincible or not last_health or not health:
|
||||
ctx.death_state = DeathState.dead
|
||||
ctx.last_death_link = time.time()
|
||||
return
|
||||
if not invincible[0] and last_health[0] == health[0]:
|
||||
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
|
||||
snes_buffered_write(ctx, WRAM_START + 0x0373,
|
||||
bytes([8])) # deal 1 full heart of damage at next opportunity
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if not gamemode or gamemode[0] in DEATH_MODES:
|
||||
ctx.death_state = DeathState.dead
|
||||
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
||||
|
||||
rom_name = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"AP":
|
||||
return False
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b001 # full local
|
||||
|
||||
ctx.rom = rom_name
|
||||
|
||||
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
|
||||
if death_link:
|
||||
ctx.allow_collect = bool(death_link[0] & 0b100)
|
||||
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = gamemode[0] in DEATH_MODES
|
||||
await ctx.handle_deathlink_state(currently_dead)
|
||||
|
||||
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
|
||||
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
|
||||
if gamemode is None or gameend is None or game_timer is None or \
|
||||
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
|
||||
return
|
||||
|
||||
if gameend[0]:
|
||||
if not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
if gamemode in ENDGAME_MODES: # triforce room and credits
|
||||
return
|
||||
|
||||
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2]
|
||||
roomid = data[4] | (data[5] << 8)
|
||||
roomdata = data[6]
|
||||
scout_location = data[7]
|
||||
|
||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||
item = ctx.items_received[recv_index]
|
||||
recv_index += 1
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names[item.item], 'red', 'bold'),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
|
||||
|
||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
|
||||
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_ADDR,
|
||||
bytes([item.item]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR,
|
||||
bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0]))
|
||||
if scout_location > 0 and scout_location in ctx.locations_info:
|
||||
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR,
|
||||
bytes([scout_location]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR,
|
||||
bytes([ctx.locations_info[scout_location].item]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR,
|
||||
bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location].player)]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
||||
ctx.locations_scouted.add(scout_location)
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
|
||||
await track_locations(ctx, roomid, roomdata)
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from BaseClasses import Region, RegionType, ItemClassification
|
||||
from worlds.alttp.SubClasses import ALttPLocation
|
||||
from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops
|
||||
from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations
|
||||
from worlds.alttp.Bosses import place_bosses
|
||||
from worlds.alttp.Dungeons import get_dungeon_item_pool_player
|
||||
from worlds.alttp.EntranceShuffle import connect_entrance
|
||||
@@ -436,12 +436,13 @@ def generate_itempool(world):
|
||||
|
||||
if world.shop_shuffle[player]:
|
||||
shuffle_shops(world, nonprogressionitems, player)
|
||||
create_dynamic_shop_locations(world, player)
|
||||
|
||||
world.itempool += progressionitems + nonprogressionitems
|
||||
|
||||
if world.retro_caves[player]:
|
||||
set_up_take_anys(world, player) # depends on world.itempool to be set
|
||||
# set_up_take_anys needs to run first
|
||||
create_dynamic_shop_locations(world, player)
|
||||
|
||||
|
||||
take_any_locations = {
|
||||
@@ -487,7 +488,7 @@ def set_up_take_anys(world, player):
|
||||
world.itempool.append(ItemFactory('Rupees (20)', player))
|
||||
old_man_take_any.shop.add_inventory(0, sword.name, 0, 0, create_location=True)
|
||||
else:
|
||||
old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0)
|
||||
old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0, create_location=True)
|
||||
|
||||
for num in range(4):
|
||||
take_any = Region("Take-Any #{}".format(num+1), RegionType.Cave, 'a cave of choice', player)
|
||||
@@ -501,29 +502,11 @@ def set_up_take_anys(world, player):
|
||||
take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True, total_shop_slots + num + 1)
|
||||
world.shops.append(take_any.shop)
|
||||
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
|
||||
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
|
||||
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=True)
|
||||
|
||||
world.initialize_regions()
|
||||
|
||||
|
||||
def create_dynamic_shop_locations(world, player):
|
||||
for shop in world.shops:
|
||||
if shop.region.player == player:
|
||||
for i, item in enumerate(shop.inventory):
|
||||
if item is None:
|
||||
continue
|
||||
if item['create_location']:
|
||||
loc = ALttPLocation(player, f"{shop.region.name} {shop.slot_names[i]}", parent=shop.region)
|
||||
shop.region.locations.append(loc)
|
||||
|
||||
world.clear_location_cache()
|
||||
|
||||
world.push_item(loc, ItemFactory(item['item'], player), False)
|
||||
loc.shop_slot = i
|
||||
loc.event = True
|
||||
loc.locked = True
|
||||
|
||||
|
||||
def get_pool_core(world, player: int):
|
||||
shuffle = world.shuffle[player]
|
||||
difficulty = world.difficulty[player]
|
||||
|
||||
@@ -20,7 +20,7 @@ def GetBeemizerItem(world, player: int, item):
|
||||
|
||||
|
||||
# should be replaced with direct world.create_item(item) call in the future
|
||||
def ItemFactory(items, player: int):
|
||||
def ItemFactory(items: typing.Union[str, typing.Iterable[str]], player: int):
|
||||
from worlds.alttp import ALTTPWorld
|
||||
world = ALTTPWorld(None, player)
|
||||
ret = []
|
||||
|
||||
@@ -686,142 +686,296 @@ lookup_name_to_id = {name: data[0] for name, data in location_table.items() if t
|
||||
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}}
|
||||
lookup_name_to_id.update(shop_table_by_location)
|
||||
|
||||
lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks',
|
||||
1573194: 'Kings Grave Inner Rocks', 1573189: 'Kings Grave Inner Rocks',
|
||||
212328: 'Kings Grave Inner Rocks', 60175: 'Blinds Hideout',
|
||||
60178: 'Blinds Hideout', 60181: 'Blinds Hideout', 60184: 'Blinds Hideout',
|
||||
60187: 'Blinds Hideout', 188229: 'Hyrule Castle Secret Entrance Drop',
|
||||
59761: 'Hyrule Castle Secret Entrance Drop', 975299: 'Zoras River',
|
||||
1573193: 'Zoras River', 59824: 'Waterfall of Wishing',
|
||||
59857: 'Waterfall of Wishing', 59770: 'Kings Grave', 59788: 'Dam',
|
||||
59836: 'Links House', 59854: 'Tavern North', 59881: 'Chicken House',
|
||||
59890: 'Aginahs Cave', 60034: 'Sahasrahlas Hut', 60037: 'Sahasrahlas Hut',
|
||||
60040: 'Sahasrahlas Hut', 193020: 'Sahasrahlas Hut',
|
||||
60046: 'Kakariko Well Drop', 60049: 'Kakariko Well Drop',
|
||||
60052: 'Kakariko Well Drop', 60055: 'Kakariko Well Drop',
|
||||
60058: 'Kakariko Well Drop', 1572906: 'Blacksmiths Hut',
|
||||
1572885: 'Bat Cave Drop', 211407: 'Sick Kids House',
|
||||
212605: 'Hobo Bridge', 1572864: 'Lost Woods Hideout Drop',
|
||||
1572865: 'Lumberjack Tree Tree', 1572867: 'Cave 45',
|
||||
1572868: 'Graveyard Cave', 1572869: 'Checkerboard Cave',
|
||||
60226: 'Mini Moldorm Cave', 60229: 'Mini Moldorm Cave',
|
||||
60232: 'Mini Moldorm Cave', 60235: 'Mini Moldorm Cave',
|
||||
1572880: 'Mini Moldorm Cave', 60238: 'Ice Rod Cave',
|
||||
60223: 'Bonk Rock Cave', 1572882: 'Library', 1572884: 'Potion Shop',
|
||||
1573188: 'Lake Hylia Island Mirror Spot',
|
||||
1573186: 'Maze Race Mirror Spot', 1573187: 'Desert Ledge Return Rocks',
|
||||
59791: 'Desert Palace Entrance (West)',
|
||||
1573216: 'Desert Palace Entrance (West)',
|
||||
59830: 'Desert Palace Entrance (West)',
|
||||
59851: 'Desert Palace Entrance (West)',
|
||||
59842: 'Desert Palace Entrance (West)',
|
||||
1573201: 'Desert Palace Entrance (North)', 59767: 'Eastern Palace',
|
||||
59773: 'Eastern Palace', 59827: 'Eastern Palace', 59833: 'Eastern Palace',
|
||||
59893: 'Eastern Palace', 1573200: 'Eastern Palace',
|
||||
166320: 'Master Sword Meadow', 59764: 'Hyrule Castle Entrance (South)',
|
||||
60172: 'Hyrule Castle Entrance (South)',
|
||||
60169: 'Hyrule Castle Entrance (South)',
|
||||
59758: 'Hyrule Castle Entrance (South)',
|
||||
60253: 'Hyrule Castle Entrance (South)',
|
||||
60256: 'Hyrule Castle Entrance (South)',
|
||||
60259: 'Hyrule Castle Entrance (South)', 60025: 'Sanctuary S&Q',
|
||||
60085: 'Agahnims Tower', 60082: 'Agahnims Tower',
|
||||
1010170: 'Old Man Cave (West)', 1572866: 'Spectacle Rock Cave',
|
||||
60202: 'Paradox Cave (Bottom)', 60205: 'Paradox Cave (Bottom)',
|
||||
60208: 'Paradox Cave (Bottom)', 60211: 'Paradox Cave (Bottom)',
|
||||
60214: 'Paradox Cave (Bottom)', 60217: 'Paradox Cave (Bottom)',
|
||||
60220: 'Paradox Cave (Bottom)', 59839: 'Spiral Cave',
|
||||
1572886: 'Death Mountain (Top)', 1573184: 'Spectacle Rock Mirror Spot',
|
||||
1573218: 'Tower of Hera', 59821: 'Tower of Hera', 59878: 'Tower of Hera',
|
||||
59899: 'Tower of Hera', 59896: 'Tower of Hera', 1573202: 'Tower of Hera',
|
||||
1573191: 'Top of Pyramid', 975237: 'Catfish Entrance Rock',
|
||||
209095: 'South Dark World Bridge', 1573192: 'South Dark World Bridge',
|
||||
1572887: 'Bombos Tablet Mirror Spot', 60190: 'Hype Cave',
|
||||
60193: 'Hype Cave', 60196: 'Hype Cave', 60199: 'Hype Cave',
|
||||
1572881: 'Hype Cave', 1572870: 'Dark World Hammer Peg Cave',
|
||||
59776: 'Pyramid Fairy', 59779: 'Pyramid Fairy', 59884: 'Brewery',
|
||||
59887: 'C-Shaped House', 60840: 'Chest Game',
|
||||
1573190: 'Bumper Cave (Bottom)', 60019: 'Mire Shed', 60022: 'Mire Shed',
|
||||
60028: 'Superbunny Cave (Top)', 60031: 'Superbunny Cave (Top)',
|
||||
60043: 'Spike Cave', 60241: 'Hookshot Cave', 60244: 'Hookshot Cave',
|
||||
60250: 'Hookshot Cave', 60247: 'Hookshot Cave',
|
||||
1573185: 'Floating Island Mirror Spot', 59845: 'Mimic Cave',
|
||||
60061: 'Swamp Palace', 59782: 'Swamp Palace', 59785: 'Swamp Palace',
|
||||
60064: 'Swamp Palace', 60070: 'Swamp Palace', 60067: 'Swamp Palace',
|
||||
60073: 'Swamp Palace', 60076: 'Swamp Palace', 60079: 'Swamp Palace',
|
||||
1573204: 'Swamp Palace', 59908: 'Thieves Town', 59905: 'Thieves Town',
|
||||
59911: 'Thieves Town', 59914: 'Thieves Town', 59917: 'Thieves Town',
|
||||
59920: 'Thieves Town', 59923: 'Thieves Town', 1573206: 'Thieves Town',
|
||||
59803: 'Skull Woods First Section Door',
|
||||
59848: 'Skull Woods First Section Hole (East)',
|
||||
59794: 'Skull Woods First Section Hole (West)',
|
||||
59809: 'Skull Woods First Section Hole (West)',
|
||||
59800: 'Skull Woods First Section Hole (North)',
|
||||
59806: 'Skull Woods Second Section Door (East)',
|
||||
59902: 'Skull Woods Final Section', 1573205: 'Skull Woods Final Section',
|
||||
59860: 'Ice Palace', 59797: 'Ice Palace', 59818: 'Ice Palace',
|
||||
59875: 'Ice Palace', 59872: 'Ice Palace', 59812: 'Ice Palace',
|
||||
59869: 'Ice Palace', 1573207: 'Ice Palace', 60007: 'Misery Mire',
|
||||
60010: 'Misery Mire', 59998: 'Misery Mire', 60001: 'Misery Mire',
|
||||
59866: 'Misery Mire', 60004: 'Misery Mire', 60013: 'Misery Mire',
|
||||
1573208: 'Misery Mire', 59938: 'Turtle Rock', 59932: 'Turtle Rock',
|
||||
59935: 'Turtle Rock', 59926: 'Turtle Rock',
|
||||
59941: 'Dark Death Mountain Ledge (West)',
|
||||
59929: 'Dark Death Mountain Ledge (East)',
|
||||
59956: 'Dark Death Mountain Ledge (West)',
|
||||
59953: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59950: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59947: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59944: 'Turtle Rock Isolated Ledge Entrance',
|
||||
1573209: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59995: 'Palace of Darkness', 59965: 'Palace of Darkness',
|
||||
59977: 'Palace of Darkness', 59959: 'Palace of Darkness',
|
||||
59962: 'Palace of Darkness', 59986: 'Palace of Darkness',
|
||||
59971: 'Palace of Darkness', 59980: 'Palace of Darkness',
|
||||
59983: 'Palace of Darkness', 59989: 'Palace of Darkness',
|
||||
59992: 'Palace of Darkness', 59968: 'Palace of Darkness',
|
||||
59974: 'Palace of Darkness', 1573203: 'Palace of Darkness',
|
||||
1573217: 'Ganons Tower', 60121: 'Ganons Tower', 60124: 'Ganons Tower',
|
||||
60130: 'Ganons Tower', 60133: 'Ganons Tower', 60136: 'Ganons Tower',
|
||||
60139: 'Ganons Tower', 60142: 'Ganons Tower', 60088: 'Ganons Tower',
|
||||
60091: 'Ganons Tower', 60094: 'Ganons Tower', 60097: 'Ganons Tower',
|
||||
60115: 'Ganons Tower', 60112: 'Ganons Tower', 60100: 'Ganons Tower',
|
||||
60103: 'Ganons Tower', 60106: 'Ganons Tower', 60109: 'Ganons Tower',
|
||||
60127: 'Ganons Tower', 60118: 'Ganons Tower', 60148: 'Ganons Tower',
|
||||
60151: 'Ganons Tower', 60145: 'Ganons Tower', 60157: 'Ganons Tower',
|
||||
60160: 'Ganons Tower', 60163: 'Ganons Tower', 60166: 'Ganons Tower',
|
||||
0x140037: 'Hyrule Castle Entrance (South)',
|
||||
0x140034: 'Hyrule Castle Entrance (South)',
|
||||
0x14000d: 'Hyrule Castle Entrance (South)',
|
||||
0x14003d: 'Hyrule Castle Entrance (South)',
|
||||
0x14005b: 'Eastern Palace', 0x140049: 'Eastern Palace',
|
||||
0x140031: 'Desert Palace Entrance (North)',
|
||||
0x14002b: 'Desert Palace Entrance (North)',
|
||||
0x140028: 'Desert Palace Entrance (North)',
|
||||
0x140061: 'Agahnims Tower', 0x140052: 'Agahnims Tower',
|
||||
0x140019: 'Swamp Palace', 0x140016: 'Swamp Palace', 0x140013: 'Swamp Palace',
|
||||
0x140010: 'Swamp Palace', 0x14000a: 'Swamp Palace',
|
||||
0x14002e: 'Skull Woods Second Section Door (East)',
|
||||
0x14001c: 'Skull Woods Final Section',
|
||||
0x14005e: 'Thieves Town', 0x14004f: 'Thieves Town',
|
||||
0x140004: 'Ice Palace', 0x140022: 'Ice Palace',
|
||||
0x140025: 'Ice Palace', 0x140046: 'Ice Palace',
|
||||
0x140055: 'Misery Mire', 0x14004c: 'Misery Mire',
|
||||
0x140064: 'Misery Mire',
|
||||
0x140058: 'Turtle Rock', 0x140007: 'Dark Death Mountain Ledge (West)',
|
||||
0x140040: 'Ganons Tower', 0x140043: 'Ganons Tower',
|
||||
0x14003a: 'Ganons Tower', 0x14001f: 'Ganons Tower',
|
||||
0x400000: 'Cave Shop (Dark Death Mountain)', 0x400001: 'Cave Shop (Dark Death Mountain)', 0x400002: 'Cave Shop (Dark Death Mountain)',
|
||||
0x400003: 'Red Shield Shop', 0x400004: 'Red Shield Shop', 0x400005: 'Red Shield Shop',
|
||||
0x400006: 'Dark Lake Hylia Shop', 0x400007: 'Dark Lake Hylia Shop', 0x400008: 'Dark Lake Hylia Shop',
|
||||
0x400009: 'Dark World Lumberjack Shop', 0x40000a: 'Dark World Lumberjack Shop', 0x40000b: 'Dark World Lumberjack Shop',
|
||||
0x40000c: 'Village of Outcasts Shop', 0x40000d: 'Village of Outcasts Shop', 0x40000e: 'Village of Outcasts Shop',
|
||||
0x40000f: 'Dark World Potion Shop', 0x400010: 'Dark World Potion Shop', 0x400011: 'Dark World Potion Shop',
|
||||
0x400012: 'Light World Death Mountain Shop', 0x400013: 'Light World Death Mountain Shop', 0x400014: 'Light World Death Mountain Shop',
|
||||
0x400015: 'Kakariko Shop', 0x400016: 'Kakariko Shop', 0x400017: 'Kakariko Shop',
|
||||
0x400018: 'Cave Shop (Lake Hylia)', 0x400019: 'Cave Shop (Lake Hylia)', 0x40001a: 'Cave Shop (Lake Hylia)',
|
||||
0x40001b: 'Potion Shop', 0x40001c: 'Potion Shop', 0x40001d: 'Potion Shop',
|
||||
0x40001e: 'Capacity Upgrade', 0x40001f: 'Capacity Upgrade', 0x400020: 'Capacity Upgrade'}
|
||||
lookup_vanilla_location_to_entrance = {
|
||||
59758: 'Hyrule Castle Entrance (South)',
|
||||
59761: 'Hyrule Castle Secret Entrance Drop',
|
||||
59764: 'Hyrule Castle Entrance (South)',
|
||||
59767: 'Eastern Palace',
|
||||
59770: 'Kings Grave',
|
||||
59773: 'Eastern Palace',
|
||||
59776: 'Pyramid Fairy',
|
||||
59779: 'Pyramid Fairy',
|
||||
59782: 'Swamp Palace',
|
||||
59785: 'Swamp Palace',
|
||||
59788: 'Dam',
|
||||
59791: 'Desert Palace Entrance (West)',
|
||||
59794: 'Skull Woods First Section Hole (West)',
|
||||
59797: 'Ice Palace',
|
||||
59800: 'Skull Woods First Section Hole (North)',
|
||||
59803: 'Skull Woods First Section Door',
|
||||
59806: 'Skull Woods Second Section Door (East)',
|
||||
59809: 'Skull Woods First Section Hole (West)',
|
||||
59812: 'Ice Palace',
|
||||
59818: 'Ice Palace',
|
||||
59821: 'Tower of Hera',
|
||||
59824: 'Waterfall of Wishing',
|
||||
59827: 'Eastern Palace',
|
||||
59830: 'Desert Palace Entrance (West)',
|
||||
59833: 'Eastern Palace',
|
||||
59836: 'Links House',
|
||||
59839: 'Spiral Cave',
|
||||
59842: 'Desert Palace Entrance (West)',
|
||||
59845: 'Mimic Cave',
|
||||
59848: 'Skull Woods First Section Hole (East)',
|
||||
59851: 'Desert Palace Entrance (West)',
|
||||
59854: 'Tavern North',
|
||||
59857: 'Waterfall of Wishing',
|
||||
59860: 'Ice Palace',
|
||||
59866: 'Misery Mire',
|
||||
59869: 'Ice Palace',
|
||||
59872: 'Ice Palace',
|
||||
59875: 'Ice Palace',
|
||||
59878: 'Tower of Hera',
|
||||
59881: 'Chicken House',
|
||||
59884: 'Brewery',
|
||||
59887: 'C-Shaped House',
|
||||
59890: 'Aginahs Cave',
|
||||
59893: 'Eastern Palace',
|
||||
59896: 'Tower of Hera',
|
||||
59899: 'Tower of Hera',
|
||||
59902: 'Skull Woods Final Section',
|
||||
59905: 'Thieves Town',
|
||||
59908: 'Thieves Town',
|
||||
59911: 'Thieves Town',
|
||||
59914: 'Thieves Town',
|
||||
59917: 'Thieves Town',
|
||||
59920: 'Thieves Town',
|
||||
59923: 'Thieves Town',
|
||||
59926: 'Turtle Rock',
|
||||
59929: 'Dark Death Mountain Ledge (East)',
|
||||
59932: 'Turtle Rock',
|
||||
59935: 'Turtle Rock',
|
||||
59938: 'Turtle Rock',
|
||||
59941: 'Dark Death Mountain Ledge (West)',
|
||||
59944: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59947: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59950: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59953: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59956: 'Dark Death Mountain Ledge (West)',
|
||||
59959: 'Palace of Darkness',
|
||||
59962: 'Palace of Darkness',
|
||||
59965: 'Palace of Darkness',
|
||||
59968: 'Palace of Darkness',
|
||||
59971: 'Palace of Darkness',
|
||||
59974: 'Palace of Darkness',
|
||||
59977: 'Palace of Darkness',
|
||||
59980: 'Palace of Darkness',
|
||||
59983: 'Palace of Darkness',
|
||||
59986: 'Palace of Darkness',
|
||||
59989: 'Palace of Darkness',
|
||||
59992: 'Palace of Darkness',
|
||||
59995: 'Palace of Darkness',
|
||||
59998: 'Misery Mire',
|
||||
60001: 'Misery Mire',
|
||||
60004: 'Misery Mire',
|
||||
60007: 'Misery Mire',
|
||||
60010: 'Misery Mire',
|
||||
60013: 'Misery Mire',
|
||||
60019: 'Mire Shed',
|
||||
60022: 'Mire Shed',
|
||||
60025: 'Sanctuary S&Q',
|
||||
60028: 'Superbunny Cave (Top)',
|
||||
60031: 'Superbunny Cave (Top)',
|
||||
60034: 'Sahasrahlas Hut',
|
||||
60037: 'Sahasrahlas Hut',
|
||||
60040: 'Sahasrahlas Hut',
|
||||
60043: 'Spike Cave',
|
||||
60046: 'Kakariko Well Drop',
|
||||
60049: 'Kakariko Well Drop',
|
||||
60052: 'Kakariko Well Drop',
|
||||
60055: 'Kakariko Well Drop',
|
||||
60058: 'Kakariko Well Drop',
|
||||
60061: 'Swamp Palace',
|
||||
60064: 'Swamp Palace',
|
||||
60067: 'Swamp Palace',
|
||||
60070: 'Swamp Palace',
|
||||
60073: 'Swamp Palace',
|
||||
60076: 'Swamp Palace',
|
||||
60079: 'Swamp Palace',
|
||||
60082: 'Agahnims Tower',
|
||||
60085: 'Agahnims Tower',
|
||||
60088: 'Ganons Tower',
|
||||
60091: 'Ganons Tower',
|
||||
60094: 'Ganons Tower',
|
||||
60097: 'Ganons Tower',
|
||||
60100: 'Ganons Tower',
|
||||
60103: 'Ganons Tower',
|
||||
60106: 'Ganons Tower',
|
||||
60109: 'Ganons Tower',
|
||||
60112: 'Ganons Tower',
|
||||
60115: 'Ganons Tower',
|
||||
60118: 'Ganons Tower',
|
||||
60121: 'Ganons Tower',
|
||||
60124: 'Ganons Tower',
|
||||
60127: 'Ganons Tower',
|
||||
60130: 'Ganons Tower',
|
||||
60133: 'Ganons Tower',
|
||||
60136: 'Ganons Tower',
|
||||
60139: 'Ganons Tower',
|
||||
60142: 'Ganons Tower',
|
||||
60145: 'Ganons Tower',
|
||||
60148: 'Ganons Tower',
|
||||
60151: 'Ganons Tower',
|
||||
60157: 'Ganons Tower',
|
||||
60160: 'Ganons Tower',
|
||||
60163: 'Ganons Tower',
|
||||
60166: 'Ganons Tower',
|
||||
60169: 'Hyrule Castle Entrance (South)',
|
||||
60172: 'Hyrule Castle Entrance (South)',
|
||||
60175: 'Blinds Hideout',
|
||||
60178: 'Blinds Hideout',
|
||||
60181: 'Blinds Hideout',
|
||||
60184: 'Blinds Hideout',
|
||||
60187: 'Blinds Hideout',
|
||||
60190: 'Hype Cave',
|
||||
60193: 'Hype Cave',
|
||||
60196: 'Hype Cave',
|
||||
60199: 'Hype Cave',
|
||||
60202: 'Paradox Cave (Bottom)',
|
||||
60205: 'Paradox Cave (Bottom)',
|
||||
60208: 'Paradox Cave (Bottom)',
|
||||
60211: 'Paradox Cave (Bottom)',
|
||||
60214: 'Paradox Cave (Bottom)',
|
||||
60217: 'Paradox Cave (Bottom)',
|
||||
60220: 'Paradox Cave (Bottom)',
|
||||
60223: 'Bonk Rock Cave',
|
||||
60226: 'Mini Moldorm Cave',
|
||||
60229: 'Mini Moldorm Cave',
|
||||
60232: 'Mini Moldorm Cave',
|
||||
60235: 'Mini Moldorm Cave',
|
||||
60238: 'Ice Rod Cave',
|
||||
60241: 'Hookshot Cave',
|
||||
60244: 'Hookshot Cave',
|
||||
60247: 'Hookshot Cave',
|
||||
60250: 'Hookshot Cave',
|
||||
60253: 'Hyrule Castle Entrance (South)',
|
||||
60256: 'Hyrule Castle Entrance (South)',
|
||||
60259: 'Hyrule Castle Entrance (South)',
|
||||
60840: 'Chest Game',
|
||||
166320: 'Master Sword Meadow',
|
||||
188229: 'Hyrule Castle Secret Entrance Drop',
|
||||
191256: 'Kings Grave Inner Rocks',
|
||||
193020: 'Sahasrahlas Hut',
|
||||
209095: 'South Dark World Bridge',
|
||||
211407: 'Sick Kids House',
|
||||
212328: 'Kings Grave Inner Rocks',
|
||||
212605: 'Hobo Bridge',
|
||||
975237: 'Catfish Entrance Rock',
|
||||
975299: 'Zoras River',
|
||||
1010170: 'Old Man Cave (West)',
|
||||
1310724: 'Ice Palace',
|
||||
1310727: 'Dark Death Mountain Ledge (West)',
|
||||
1310730: 'Swamp Palace',
|
||||
1310733: 'Hyrule Castle Entrance (South)',
|
||||
1310736: 'Swamp Palace',
|
||||
1310739: 'Swamp Palace',
|
||||
1310742: 'Swamp Palace',
|
||||
1310745: 'Swamp Palace',
|
||||
1310748: 'Skull Woods Final Section',
|
||||
1310751: 'Ganons Tower',
|
||||
1310754: 'Ice Palace',
|
||||
1310757: 'Ice Palace',
|
||||
1310760: 'Desert Palace Entrance (North)',
|
||||
1310763: 'Desert Palace Entrance (North)',
|
||||
1310766: 'Skull Woods Second Section Door (East)',
|
||||
1310769: 'Desert Palace Entrance (North)',
|
||||
1310772: 'Hyrule Castle Entrance (South)',
|
||||
1310775: 'Hyrule Castle Entrance (South)',
|
||||
1310778: 'Ganons Tower',
|
||||
1310781: 'Hyrule Castle Entrance (South)',
|
||||
1310784: 'Ganons Tower',
|
||||
1310787: 'Ganons Tower',
|
||||
1310790: 'Ice Palace',
|
||||
1310793: 'Eastern Palace',
|
||||
1310796: 'Misery Mire',
|
||||
1310799: 'Thieves Town',
|
||||
1310802: 'Agahnims Tower',
|
||||
1310805: 'Misery Mire',
|
||||
1310808: 'Turtle Rock',
|
||||
1310811: 'Eastern Palace',
|
||||
1310814: 'Thieves Town',
|
||||
1310817: 'Agahnims Tower',
|
||||
1310820: 'Misery Mire',
|
||||
1572864: 'Lost Woods Hideout Drop',
|
||||
1572865: 'Lumberjack Tree Tree',
|
||||
1572866: 'Spectacle Rock Cave',
|
||||
1572867: 'Cave 45',
|
||||
1572868: 'Graveyard Cave',
|
||||
1572869: 'Checkerboard Cave',
|
||||
1572870: 'Dark World Hammer Peg Cave',
|
||||
1572880: 'Mini Moldorm Cave',
|
||||
1572881: 'Hype Cave',
|
||||
1572882: 'Library',
|
||||
1572883: 'Kings Grave Inner Rocks',
|
||||
1572884: 'Potion Shop',
|
||||
1572885: 'Bat Cave Drop',
|
||||
1572886: 'Death Mountain (Top)',
|
||||
1572887: 'Bombos Tablet Mirror Spot',
|
||||
1572906: 'Blacksmiths Hut',
|
||||
1573184: 'Spectacle Rock Mirror Spot',
|
||||
1573185: 'Floating Island Mirror Spot',
|
||||
1573186: 'Maze Race Mirror Spot',
|
||||
1573187: 'Desert Ledge Return Rocks',
|
||||
1573188: 'Lake Hylia Island Mirror Spot',
|
||||
1573189: 'Kings Grave Inner Rocks',
|
||||
1573190: 'Bumper Cave (Bottom)',
|
||||
1573191: 'Top of Pyramid',
|
||||
1573192: 'South Dark World Bridge',
|
||||
1573193: 'Zoras River',
|
||||
1573194: 'Kings Grave Inner Rocks',
|
||||
1573200: 'Eastern Palace',
|
||||
1573201: 'Desert Palace Entrance (North)',
|
||||
1573202: 'Tower of Hera',
|
||||
1573203: 'Palace of Darkness',
|
||||
1573204: 'Swamp Palace',
|
||||
1573205: 'Skull Woods Final Section',
|
||||
1573206: 'Thieves Town',
|
||||
1573207: 'Ice Palace',
|
||||
1573208: 'Misery Mire',
|
||||
1573209: 'Turtle Rock Isolated Ledge Entrance',
|
||||
1573216: 'Desert Palace Entrance (West)',
|
||||
1573217: 'Ganons Tower',
|
||||
1573218: 'Tower of Hera',
|
||||
4194304: 'Cave Shop (Dark Death Mountain)',
|
||||
4194305: 'Cave Shop (Dark Death Mountain)',
|
||||
4194306: 'Cave Shop (Dark Death Mountain)',
|
||||
4194307: 'Red Shield Shop',
|
||||
4194308: 'Red Shield Shop',
|
||||
4194309: 'Red Shield Shop',
|
||||
4194310: 'Dark Lake Hylia Shop',
|
||||
4194311: 'Dark Lake Hylia Shop',
|
||||
4194312: 'Dark Lake Hylia Shop',
|
||||
4194313: 'Dark World Lumberjack Shop',
|
||||
4194314: 'Dark World Lumberjack Shop',
|
||||
4194315: 'Dark World Lumberjack Shop',
|
||||
4194316: 'Village of Outcasts Shop',
|
||||
4194317: 'Village of Outcasts Shop',
|
||||
4194318: 'Village of Outcasts Shop',
|
||||
4194319: 'Dark World Potion Shop',
|
||||
4194320: 'Dark World Potion Shop',
|
||||
4194321: 'Dark World Potion Shop',
|
||||
4194322: 'Light World Death Mountain Shop',
|
||||
4194323: 'Light World Death Mountain Shop',
|
||||
4194324: 'Light World Death Mountain Shop',
|
||||
4194325: 'Kakariko Shop',
|
||||
4194326: 'Kakariko Shop',
|
||||
4194327: 'Kakariko Shop',
|
||||
4194328: 'Cave Shop (Lake Hylia)',
|
||||
4194329: 'Cave Shop (Lake Hylia)',
|
||||
4194330: 'Cave Shop (Lake Hylia)',
|
||||
4194331: 'Potion Shop',
|
||||
4194332: 'Potion Shop',
|
||||
4194333: 'Potion Shop',
|
||||
4194334: 'Capacity Upgrade',
|
||||
4194335: 'Capacity Upgrade',
|
||||
4194336: 'Capacity Upgrade',
|
||||
# have no vanilla entrance
|
||||
4194337: "Old Man Sword Cave",
|
||||
4194338: "Take-Any #1",
|
||||
4194339: "Take-Any #2",
|
||||
4194340: "Take-Any #3",
|
||||
4194341: "Take-Any #4",
|
||||
}
|
||||
|
||||
lookup_prizes = {location for location in location_table if location.endswith(" - Prize")}
|
||||
lookup_boss_drops = {location for location in location_table if location.endswith(" - Boss")}
|
||||
@@ -2304,7 +2304,7 @@ def write_strings(rom, world, player):
|
||||
'dungeonscrossed'] else 8
|
||||
hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
|
||||
if hint_count:
|
||||
locations = world.find_items_in_locations(items_to_hint, player)
|
||||
locations = world.find_items_in_locations(items_to_hint, player, True)
|
||||
local_random.shuffle(locations)
|
||||
for x in range(min(hint_count, len(locations))):
|
||||
this_location = locations.pop()
|
||||
@@ -2321,7 +2321,7 @@ def write_strings(rom, world, player):
|
||||
|
||||
# We still need the older hints of course. Those are done here.
|
||||
|
||||
silverarrows = world.find_item_locations('Silver Bow', player)
|
||||
silverarrows = world.find_item_locations('Silver Bow', player, True)
|
||||
local_random.shuffle(silverarrows)
|
||||
silverarrow_hint = (
|
||||
' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!'
|
||||
@@ -2329,13 +2329,13 @@ def write_strings(rom, world, player):
|
||||
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
if world.worlds[player].has_progressive_bows and (world.difficulty_requirements[player].progressive_bow_limit >= 2 or (
|
||||
world.swordless[player] or world.logic[player] == 'noglitches')):
|
||||
prog_bow_locs = world.find_item_locations('Progressive Bow', player)
|
||||
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
|
||||
world.slot_seeds[player].shuffle(prog_bow_locs)
|
||||
found_bow = False
|
||||
found_bow_alt = False
|
||||
while prog_bow_locs and not (found_bow and found_bow_alt):
|
||||
bow_loc = prog_bow_locs.pop()
|
||||
if bow_loc.item.code == 0x65:
|
||||
if bow_loc.item.code == 0x65 or (found_bow and not prog_bow_locs):
|
||||
found_bow_alt = True
|
||||
target = 'ganon_phase_3_no_silvers'
|
||||
else:
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import collections
|
||||
import logging
|
||||
from typing import Iterator, Set
|
||||
|
||||
from worlds.alttp import OverworldGlitchRules
|
||||
from BaseClasses import RegionType, MultiWorld, Entrance
|
||||
from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups
|
||||
from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups, item_table
|
||||
from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules
|
||||
from worlds.alttp.Regions import location_table
|
||||
from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules
|
||||
from worlds.alttp.Bosses import GanonDefeatRule
|
||||
from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \
|
||||
@@ -176,6 +179,14 @@ def dungeon_boss_rules(world, player):
|
||||
def global_rules(world, player):
|
||||
# ganon can only carry triforce
|
||||
add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player)
|
||||
# dungeon prizes can only be crystals/pendants
|
||||
crystals_and_pendants: Set[str] = \
|
||||
{item for item, item_data in item_table.items() if item_data.type == "Crystal"}
|
||||
prize_locations: Iterator[str] = \
|
||||
(locations for locations, location_data in location_table.items() if location_data[2] == True)
|
||||
for prize_location in prize_locations:
|
||||
add_item_rule(world.get_location(prize_location, player),
|
||||
lambda item: item.name in crystals_and_pendants and item.player == player)
|
||||
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
|
||||
world.get_region('Menu', player).can_reach_private = lambda state: True
|
||||
for exit in world.get_region('Menu', player).exits:
|
||||
@@ -515,7 +526,7 @@ def default_rules(world, player):
|
||||
set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
|
||||
|
||||
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
|
||||
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player].to_bool(world, player))
|
||||
|
||||
if world.swordless[player]:
|
||||
swordless_rules(world, player)
|
||||
|
||||
@@ -39,9 +39,9 @@ class Shop():
|
||||
blacklist: Set[str] = set() # items that don't work, todo: actually check against this
|
||||
type = ShopType.Shop
|
||||
slot_names: Dict[int, str] = {
|
||||
0: "Left",
|
||||
1: "Center",
|
||||
2: "Right"
|
||||
0: " Left",
|
||||
1: " Center",
|
||||
2: " Right"
|
||||
}
|
||||
|
||||
def __init__(self, region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool, sram_offset: int):
|
||||
@@ -142,7 +142,11 @@ class Shop():
|
||||
|
||||
class TakeAny(Shop):
|
||||
type = ShopType.TakeAny
|
||||
|
||||
slot_names: Dict[int, str] = {
|
||||
0: "",
|
||||
1: "",
|
||||
2: ""
|
||||
}
|
||||
|
||||
class UpgradeShop(Shop):
|
||||
type = ShopType.UpgradeShop
|
||||
@@ -168,8 +172,10 @@ def FillDisabledShopSlots(world):
|
||||
|
||||
|
||||
def ShopSlotFill(world):
|
||||
shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops)
|
||||
shop_slots: Set[ALttPLocation] = {location for shop_locations in
|
||||
(shop.region.locations for shop in world.shops if shop.type != ShopType.TakeAny)
|
||||
for location in shop_locations if location.shop_slot is not None}
|
||||
|
||||
removed = set()
|
||||
for location in shop_slots:
|
||||
shop: Shop = location.parent_region.shop
|
||||
@@ -318,7 +324,7 @@ def create_shops(world, player: int):
|
||||
for index, item in enumerate(inventory):
|
||||
shop.add_inventory(index, *item)
|
||||
if not locked and num_slots:
|
||||
slot_name = f"{region.name} {shop.slot_names[index]}"
|
||||
slot_name = f"{region.name}{shop.slot_names[index]}"
|
||||
loc = ALttPLocation(player, slot_name, address=shop_table_by_location[slot_name],
|
||||
parent=region, hint_text="for sale")
|
||||
loc.shop_slot = index
|
||||
@@ -376,7 +382,7 @@ total_dynamic_shop_slots = sum(3 for shopname, data in shop_table.items() if not
|
||||
|
||||
SHOP_ID_START = 0x400000
|
||||
shop_table_by_location_id = dict(enumerate(
|
||||
(f"{name} {Shop.slot_names[num]}" for name, shop_data in
|
||||
(f"{name}{Shop.slot_names[num]}" for name, shop_data in
|
||||
sorted(shop_table.items(), key=lambda item: item[1].sram_offset)
|
||||
for num in range(3)), start=SHOP_ID_START))
|
||||
|
||||
@@ -591,3 +597,22 @@ def price_to_funny_price(world, item: dict, player: int):
|
||||
item['price'] = min(price_chart[p_type](item['price']), 255)
|
||||
item['price_type'] = p_type
|
||||
break
|
||||
|
||||
|
||||
def create_dynamic_shop_locations(world, player):
|
||||
for shop in world.shops:
|
||||
if shop.region.player == player:
|
||||
for i, item in enumerate(shop.inventory):
|
||||
if item is None:
|
||||
continue
|
||||
if item['create_location']:
|
||||
slot_name = f"{shop.region.name}{shop.slot_names[i]}"
|
||||
loc = ALttPLocation(player, slot_name,
|
||||
address=shop_table_by_location[slot_name], parent=shop.region)
|
||||
loc.place_locked_item(ItemFactory(item['item'], player))
|
||||
if shop.type == ShopType.TakeAny:
|
||||
loc.shop_slot_disabled = True
|
||||
shop.region.locations.append(loc)
|
||||
world.clear_location_cache()
|
||||
|
||||
loc.shop_slot = i
|
||||
|
||||
39
worlds/alttp/test/items/TestPrizes.py
Normal file
39
worlds/alttp/test/items/TestPrizes.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import Item, Location
|
||||
from test.TestBase import WorldTestBase
|
||||
|
||||
|
||||
class TestPrizes(WorldTestBase):
|
||||
game = "A Link to the Past"
|
||||
|
||||
def test_item_rules(self):
|
||||
prize_locations: List[Location] = [
|
||||
self.multiworld.get_location("Eastern Palace - Prize", 1),
|
||||
self.multiworld.get_location("Desert Palace - Prize", 1),
|
||||
self.multiworld.get_location("Tower of Hera - Prize", 1),
|
||||
self.multiworld.get_location("Palace of Darkness - Prize", 1),
|
||||
self.multiworld.get_location("Swamp Palace - Prize", 1),
|
||||
self.multiworld.get_location("Thieves\' Town - Prize", 1),
|
||||
self.multiworld.get_location("Skull Woods - Prize", 1),
|
||||
self.multiworld.get_location("Ice Palace - Prize", 1),
|
||||
self.multiworld.get_location("Misery Mire - Prize", 1),
|
||||
self.multiworld.get_location("Turtle Rock - Prize", 1),
|
||||
]
|
||||
prize_items: List[Item] = [
|
||||
self.get_item_by_name("Green Pendant"),
|
||||
self.get_item_by_name("Blue Pendant"),
|
||||
self.get_item_by_name("Red Pendant"),
|
||||
self.get_item_by_name("Crystal 1"),
|
||||
self.get_item_by_name("Crystal 2"),
|
||||
self.get_item_by_name("Crystal 3"),
|
||||
self.get_item_by_name("Crystal 4"),
|
||||
self.get_item_by_name("Crystal 5"),
|
||||
self.get_item_by_name("Crystal 6"),
|
||||
self.get_item_by_name("Crystal 7"),
|
||||
]
|
||||
|
||||
for item in self.multiworld.get_items():
|
||||
for prize_location in prize_locations:
|
||||
self.assertEqual(item in prize_items, prize_location.item_rule(item),
|
||||
f"{item} must {'' if item in prize_items else 'not '}be allowed in {prize_location}.")
|
||||
37
worlds/alttp/test/options/TestOpenPyramid.py
Normal file
37
worlds/alttp/test/options/TestOpenPyramid.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from test.TestBase import WorldTestBase
|
||||
from ...Items import ItemFactory
|
||||
|
||||
|
||||
class PyramidTestBase(WorldTestBase):
|
||||
game = "A Link to the Past"
|
||||
|
||||
|
||||
class OpenPyramidTest(PyramidTestBase):
|
||||
options = {
|
||||
"open_pyramid": "open"
|
||||
}
|
||||
|
||||
def testAccess(self):
|
||||
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
|
||||
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
|
||||
self.assertTrue(self.can_reach_entrance("Pyramid Hole"))
|
||||
|
||||
|
||||
class GoalPyramidTest(PyramidTestBase):
|
||||
options = {
|
||||
"open_pyramid": "goal"
|
||||
}
|
||||
|
||||
def testCrystalsGoalAccess(self):
|
||||
self.multiworld.goal[1] = "crystals"
|
||||
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
|
||||
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
|
||||
self.assertTrue(self.can_reach_entrance("Pyramid Hole"))
|
||||
|
||||
def testGanonGoalAccess(self):
|
||||
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
|
||||
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
|
||||
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
|
||||
self.multiworld.state.collect(ItemFactory("Beat Agahnim 2", 1))
|
||||
self.assertTrue(self.can_reach_entrance("Pyramid Hole"))
|
||||
|
||||
@@ -113,8 +113,8 @@ class TestPlandoBosses(unittest.TestCase):
|
||||
"""Test automatic singularity mode"""
|
||||
self.assertIn(";singularity", MultiBosses.from_any("b2").value)
|
||||
|
||||
def testPlandoSettings(self):
|
||||
"""Test that plando settings verification works"""
|
||||
def testPlandoOptions(self):
|
||||
"""Test that plando options verification works"""
|
||||
plandoed_string = "l1-b2;l2-b1"
|
||||
mixed_string = "l1-b2;shuffle"
|
||||
regular_string = "shuffle"
|
||||
@@ -123,14 +123,14 @@ class TestPlandoBosses(unittest.TestCase):
|
||||
regular = MultiBosses.from_any(regular_string)
|
||||
|
||||
# plando should work with boss plando
|
||||
plandoed.verify(None, "Player", Generate.PlandoSettings.bosses)
|
||||
plandoed.verify(None, "Player", Generate.PlandoOptions.bosses)
|
||||
self.assertTrue(plandoed.value.startswith(plandoed_string))
|
||||
# plando should fall back to default without boss plando
|
||||
plandoed.verify(None, "Player", Generate.PlandoSettings.items)
|
||||
plandoed.verify(None, "Player", Generate.PlandoOptions.items)
|
||||
self.assertEqual(plandoed, MultiBosses.option_vanilla)
|
||||
# mixed should fall back to mode
|
||||
mixed.verify(None, "Player", Generate.PlandoSettings.items) # should produce a warning and still work
|
||||
mixed.verify(None, "Player", Generate.PlandoOptions.items) # should produce a warning and still work
|
||||
self.assertEqual(mixed, MultiBosses.option_shuffle)
|
||||
# mode stuff should just work
|
||||
regular.verify(None, "Player", Generate.PlandoSettings.items)
|
||||
regular.verify(None, "Player", Generate.PlandoOptions.items)
|
||||
self.assertEqual(regular, MultiBosses.option_shuffle)
|
||||
|
||||
@@ -6,7 +6,7 @@ BK Sudoku is not a typical Archipelago game; instead, it is a generic Sudoku cli
|
||||
|
||||
## What hints are unlocked?
|
||||
|
||||
After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to. It is possible to hint a location that was previously hinted for using the !hint command.
|
||||
After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to.
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
|
||||
@@ -61,6 +61,19 @@ class MaxTechCost(TechCost):
|
||||
default = 500
|
||||
|
||||
|
||||
class TechCostDistribution(Choice):
|
||||
"""Random distribution of costs of the Science Packs.
|
||||
Even: any number between min and max is equally likely.
|
||||
Low: low costs, near the minimum, are more likely.
|
||||
Middle: medium costs, near the average, are more likely.
|
||||
High: high costs, near the maximum, are more likely."""
|
||||
display_name = "Tech Cost Distribution"
|
||||
option_even = 0
|
||||
option_low = 1
|
||||
option_middle = 2
|
||||
option_high = 3
|
||||
|
||||
|
||||
class TechCostMix(Range):
|
||||
"""Percent chance that a preceding Science Pack is also required.
|
||||
Chance is rolled per preceding pack."""
|
||||
@@ -69,6 +82,14 @@ class TechCostMix(Range):
|
||||
default = 70
|
||||
|
||||
|
||||
class RampingTechCosts(Toggle):
|
||||
"""Forces the amount of Science Packs required to ramp up with the highest involved Pack. Average is preserved.
|
||||
For example:
|
||||
off: Automation (red)/Logistics (green) sciences can range from 1 to 1000 Science Packs,
|
||||
on: Automation (red) ranges to ~500 packs and Logistics (green) from ~500 to 1000 Science Packs"""
|
||||
display_name = "Ramping Tech Costs"
|
||||
|
||||
|
||||
class Silo(Choice):
|
||||
"""Ingredients to craft rocket silo or auto-place if set to spawn."""
|
||||
display_name = "Rocket Silo"
|
||||
@@ -371,7 +392,9 @@ factorio_options: typing.Dict[str, type(Option)] = {
|
||||
"tech_tree_layout": TechTreeLayout,
|
||||
"min_tech_cost": MinTechCost,
|
||||
"max_tech_cost": MaxTechCost,
|
||||
"tech_cost_distribution": TechCostDistribution,
|
||||
"tech_cost_mix": TechCostMix,
|
||||
"ramping_tech_costs": RampingTechCosts,
|
||||
"silo": Silo,
|
||||
"satellite": Satellite,
|
||||
"free_samples": FreeSamples,
|
||||
|
||||
@@ -7,7 +7,7 @@ import typing
|
||||
from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .Mod import generate_mod
|
||||
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal
|
||||
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution
|
||||
from .Shapes import get_shapes
|
||||
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
|
||||
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
|
||||
@@ -97,9 +97,24 @@ class Factorio(World):
|
||||
location_names = self.multiworld.random.sample(location_pool, location_count)
|
||||
self.locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
|
||||
for loc_name in location_names]
|
||||
rand_values = sorted(random.randint(self.multiworld.min_tech_cost[self.player],
|
||||
self.multiworld.max_tech_cost[self.player]) for _ in self.locations)
|
||||
for i, location in enumerate(sorted(self.locations, key=lambda loc: loc.rel_cost)):
|
||||
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player]
|
||||
min_cost = self.multiworld.min_tech_cost[self.player]
|
||||
max_cost = self.multiworld.max_tech_cost[self.player]
|
||||
if distribution == distribution.option_even:
|
||||
rand_values = (random.randint(min_cost, max_cost) for _ in self.locations)
|
||||
else:
|
||||
mode = {distribution.option_low: min_cost,
|
||||
distribution.option_middle: (min_cost+max_cost)//2,
|
||||
distribution.option_high: max_cost}[distribution.value]
|
||||
rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.locations)
|
||||
rand_values = sorted(rand_values)
|
||||
if self.multiworld.ramping_tech_costs[self.player]:
|
||||
def sorter(loc: FactorioScienceLocation):
|
||||
return loc.complexity, loc.rel_cost
|
||||
else:
|
||||
def sorter(loc: FactorioScienceLocation):
|
||||
return loc.rel_cost
|
||||
for i, location in enumerate(sorted(self.locations, key=sorter)):
|
||||
location.count = rand_values[i]
|
||||
del rand_values
|
||||
nauvis.locations.extend(self.locations)
|
||||
@@ -198,6 +213,10 @@ class Factorio(World):
|
||||
self.multiworld.itempool.append(tech_item)
|
||||
else:
|
||||
loc = cost_sorted_locations[index]
|
||||
if index >= 0:
|
||||
# beginning techs - limit cost to 10
|
||||
# as automation is not achievable yet and hand-crafting for hours is not fun gameplay
|
||||
loc.count = min(loc.count, 10)
|
||||
loc.place_locked_item(tech_item)
|
||||
loc.revealed = True
|
||||
|
||||
@@ -448,7 +467,7 @@ class FactorioScienceLocation(FactorioLocation):
|
||||
|
||||
# Factorio technology properties:
|
||||
ingredients: typing.Dict[str, int]
|
||||
count: int
|
||||
count: int = 0
|
||||
|
||||
def __init__(self, player: int, name: str, address: int, parent: Region):
|
||||
super(FactorioScienceLocation, self).__init__(player, name, address, parent)
|
||||
@@ -460,8 +479,6 @@ class FactorioScienceLocation(FactorioLocation):
|
||||
for complexity in range(self.complexity):
|
||||
if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99):
|
||||
self.ingredients[Factorio.ordered_science_packs[complexity]] = 1
|
||||
self.count = parent.multiworld.random.randint(parent.multiworld.min_tech_cost[self.player],
|
||||
parent.multiworld.max_tech_cost[self.player])
|
||||
|
||||
@property
|
||||
def factorio_ingredients(self) -> typing.List[typing.Tuple[str, int]]:
|
||||
|
||||
@@ -63,7 +63,14 @@ games you want settings for.
|
||||
using this to detail the intention of the file.
|
||||
|
||||
* `name` is the player name you would like to use and is used for your slot data to connect with most games. This can
|
||||
also be filled with multiple names each having a weight to it.
|
||||
also be filled with multiple names each having a weight to it. Names can also contain certain keywords, surrounded by
|
||||
curly-braces, which will be replaced on generation with a number:
|
||||
|
||||
* `{player}` will be replaced with the player's slot number.
|
||||
* `{PLAYER}` will be replaced with the player's slot number if that slot number is greater than 1, otherwise blank.
|
||||
* `{number}` will be replaced with the counter value of the name.
|
||||
* `{NUMBER}` will be replaced with the counter value of the name if the counter value is greater than 1, otherwise
|
||||
blank.
|
||||
|
||||
* `game` is where either your chosen game goes or if you would like can be filled with multiple games each with
|
||||
different weights.
|
||||
|
||||
@@ -70,10 +70,9 @@ including the exclamation point.
|
||||
names such as Factorio.
|
||||
- `!hint <item name>` Tells you at which location in whose game your Item is. Note you need to have checked some
|
||||
locations to earn a hint. You can check how many you have by just running `!hint`
|
||||
- `!forfeit` If you didn't turn on auto-forfeit or if you allowed forfeiting prior to goal completion. Remember that "
|
||||
forfeiting" actually means sending out your remaining items in your world.
|
||||
- `!collect` Grants you all the remaining checks in your world. Can only be used after your goal is complete or when you
|
||||
have forfeited.
|
||||
- `!release` If you didn't turn on auto-release or if you allowed releasing prior to goal completion. Remember that "
|
||||
releasing" actually means sending out your remaining items in your world.
|
||||
- `!collect` Grants you all the remaining checks in your world. Typically used after goal completion.
|
||||
|
||||
#### Host only (on Archipelago.gg or in your server console)
|
||||
|
||||
@@ -87,9 +86,9 @@ including the exclamation point.
|
||||
- `/exit` Shutdown the server
|
||||
- `/alias <player name> <alias name>` Assign a player an alias.
|
||||
- `/collect <player name>` Send out any items remaining in the multiworld belonging to the given player.
|
||||
- `/forfeit <player name>` Forfeits someone regardless of settings and game completion status
|
||||
- `/allow_forfeit <player name>` Allows the given player to use the `!forfeit` command.
|
||||
- `/forbid_forfeit <player name>` Bars the given player from using the `!forfeit` command.
|
||||
- `/release <player name>` Releases someone regardless of settings and game completion status
|
||||
- `/allow_release <player name>` Allows the given player to use the `!release` command.
|
||||
- `/forbid_release <player name>` Bars the given player from using the `!release` command.
|
||||
- `/send <player name> <item name>` Grants the given player the specified item.
|
||||
- `/send_multiple <amount> <player name> <item name>` Grants the given player the stated amount of the specified item.
|
||||
- `/send_location <player name> <location name>` Send out the given location for the specified player as if the player checked it
|
||||
|
||||
@@ -69,7 +69,7 @@ multiworld. The output of this process is placed in the `output` folder.
|
||||
#### Changing local host settings for generation
|
||||
|
||||
Sometimes there are various settings that you may want to change before rolling a seed such as enabling race mode,
|
||||
auto-forfeit, plando support, or setting a password.
|
||||
auto-release, plando support, or setting a password.
|
||||
|
||||
All of these settings plus other options are able to be changed by modifying the `host.yaml` file in the Archipelago
|
||||
installation folder. The settings chosen here are baked into the `.archipelago` file that gets output with the other
|
||||
|
||||
@@ -8,7 +8,7 @@ should only take a couple of minutes to read.
|
||||
1. After gathering the YAML files together in one location, select all the files and compress them into a `.ZIP` file.
|
||||
2. Next go to the "Generate Game" page. Generate game
|
||||
page: [Archipelago Seed Generation Page](/generate). Here, you can adjust some server settings
|
||||
such as forfeit rules and the cost for a player to use a hint before generation.
|
||||
such as release rules and the cost for a player to use a hint before generation.
|
||||
3. After adjusting the host settings to your liking click on the Upload File button and using the explorer window that
|
||||
opens, navigate to the location where you zipped the player files and upload this zip. The page will generate your
|
||||
game and refresh multiple times to check on completion status.
|
||||
|
||||
@@ -26,7 +26,7 @@ item_name_groups = {group: lookup_type_to_names[group] for group in ("Skill", "C
|
||||
directionals = ('', 'Left_', 'Right_')
|
||||
|
||||
item_name_groups.update({
|
||||
"Dreamer": {"Herrah", "Monomon", "Lurien"},
|
||||
"Dreamers": {"Herrah", "Monomon", "Lurien"},
|
||||
"Cloak": {x + 'Mothwing_Cloak' for x in directionals} | {'Shade_Cloak', 'Split_Shade_Cloak'},
|
||||
"Claw": {x + 'Mantis_Claw' for x in directionals},
|
||||
"CDash": {x + 'Crystal_Heart' for x in directionals},
|
||||
|
||||
@@ -48,7 +48,7 @@ At minimum, every seed will require you to find the Cursed Seal and bring it bac
|
||||
- `any_ending`: You must defeat the final boss.
|
||||
- `true_ending`: You must first explore all 3000 rooms of the Atlas Dome and find the Agate Knife, then fight the final boss' true form.
|
||||
|
||||
Once the goal has been completed, you may press F to send a forfeit, sending out all of your world's remaining items to their respective players, and C to send a collect, which gathers up all of your world's items from their shuffled locations in other player's worlds. You may also press S to view your statistics, if you're a fan of numbers.
|
||||
Once the goal has been completed, you may press F to send a release, sending out all of your world's remaining items to their respective players, and C to send a collect, which gathers up all of your world's items from their shuffled locations in other player's worlds. You may also press S to view your statistics, if you're a fan of numbers.
|
||||
|
||||
More in-depth information about the game can be found in the game's help file, accessed by pressing H while playing.
|
||||
|
||||
|
||||
@@ -458,11 +458,11 @@ def shuffle_random_entrances(ootworld):
|
||||
one_way_entrance_pools['OwlDrop'] = ootworld.get_shufflable_entrances(type='OwlDrop')
|
||||
if ootworld.warp_songs:
|
||||
one_way_entrance_pools['WarpSong'] = ootworld.get_shufflable_entrances(type='WarpSong')
|
||||
if ootworld.logic_rules == 'glitchless':
|
||||
one_way_priorities['Bolero'] = priority_entrance_table['Bolero']
|
||||
one_way_priorities['Nocturne'] = priority_entrance_table['Nocturne']
|
||||
if not ootworld.shuffle_dungeon_entrances and not ootworld.shuffle_overworld_entrances:
|
||||
one_way_priorities['Requiem'] = priority_entrance_table['Requiem']
|
||||
# No more exceptions for NL here, causes cascading failures later
|
||||
one_way_priorities['Bolero'] = priority_entrance_table['Bolero']
|
||||
one_way_priorities['Nocturne'] = priority_entrance_table['Nocturne']
|
||||
if not ootworld.shuffle_dungeon_entrances and not ootworld.shuffle_overworld_entrances:
|
||||
one_way_priorities['Requiem'] = priority_entrance_table['Requiem']
|
||||
if ootworld.spawn_positions:
|
||||
one_way_entrance_pools['Spawn'] = ootworld.get_shufflable_entrances(type='Spawn')
|
||||
if 'child' not in ootworld.spawn_positions:
|
||||
@@ -480,7 +480,7 @@ def shuffle_random_entrances(ootworld):
|
||||
if ootworld.shuffle_dungeon_entrances:
|
||||
entrance_pools['Dungeon'] = ootworld.get_shufflable_entrances(type='Dungeon', only_primary=True)
|
||||
if ootworld.open_forest == 'closed':
|
||||
entrance_pools['Dungeon'].remove(ootworld.get_entrance('KF Outside Deku Tree -> Deku Tree Lobby', player))
|
||||
entrance_pools['Dungeon'].remove(ootworld.get_entrance('KF Outside Deku Tree -> Deku Tree Lobby'))
|
||||
if ootworld.shuffle_special_dungeon_entrances:
|
||||
entrance_pools['Dungeon'] += ootworld.get_shufflable_entrances(type='DungeonSpecial', only_primary=True)
|
||||
if ootworld.decouple_entrances:
|
||||
@@ -500,7 +500,7 @@ def shuffle_random_entrances(ootworld):
|
||||
exclude_overworld_reverse = ootworld.mix_entrance_pools == 'all' and not ootworld.decouple_entrances
|
||||
entrance_pools['Overworld'] = ootworld.get_shufflable_entrances(type='Overworld', only_primary=exclude_overworld_reverse)
|
||||
if not ootworld.decouple_entrances:
|
||||
entrance_pools['Overworld'].remove(world.get_entrance('GV Lower Stream -> Lake Hylia', player))
|
||||
entrance_pools['Overworld'].remove(ootworld.get_entrance('GV Lower Stream -> Lake Hylia'))
|
||||
|
||||
# Mark shuffled entrances
|
||||
for entrance in chain(chain.from_iterable(one_way_entrance_pools.values()), chain.from_iterable(entrance_pools.values())):
|
||||
@@ -822,7 +822,7 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
|
||||
raise EntranceShuffleError(f'{entrance.name} potentially accessible as adult')
|
||||
|
||||
# Check if all locations are reachable if not NL
|
||||
if ootworld.logic_rules != 'no_logic' and locations_to_ensure_reachable:
|
||||
if locations_to_ensure_reachable:
|
||||
for loc in locations_to_ensure_reachable:
|
||||
if not all_state.can_reach(loc, 'Location', player):
|
||||
raise EntranceShuffleError(f'{loc} is unreachable')
|
||||
|
||||
@@ -1136,14 +1136,14 @@ def buildMiscItemHints(world, messages):
|
||||
if world.multiworld.state.has(data['default_item'], world.player) > 0:
|
||||
text = data['default_item_text'].format(area='#your pocket#')
|
||||
elif item_locations:
|
||||
location = item_locations[0]
|
||||
location = world.hint_rng.choice(item_locations)
|
||||
player_text = ''
|
||||
if location.player != world.player:
|
||||
player_text = world.multiworld.get_player_name(location.player) + "'s "
|
||||
if location.game == 'Ocarina of Time':
|
||||
area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.clearer_hints, world=None)
|
||||
else:
|
||||
area = location.name
|
||||
area = location.name
|
||||
text = data['default_item_text'].format(area=(player_text + area))
|
||||
elif 'default_item_fallback' in data:
|
||||
text = data['default_item_fallback']
|
||||
|
||||
@@ -41,7 +41,7 @@ class OOTItem(Item):
|
||||
classification = ItemClassification.useful
|
||||
elif name == "Ice Trap":
|
||||
classification = ItemClassification.trap
|
||||
elif name == 'Gold Skulltula Token':
|
||||
elif name in {'Gold Skulltula Token', 'Triforce Piece'}:
|
||||
classification = ItemClassification.progression_skip_balancing
|
||||
elif advancement:
|
||||
classification = ItemClassification.progression
|
||||
|
||||
@@ -109,7 +109,7 @@ class OOTWorld(World):
|
||||
|
||||
data_version = 3
|
||||
|
||||
required_client_version = (0, 3, 6)
|
||||
required_client_version = (0, 3, 7)
|
||||
|
||||
item_name_groups = {
|
||||
# internal groups
|
||||
@@ -171,12 +171,13 @@ class OOTWorld(World):
|
||||
# ER and glitched logic are not compatible; glitched takes priority
|
||||
if self.logic_rules == 'glitched':
|
||||
self.shuffle_interior_entrances = 'off'
|
||||
self.shuffle_dungeon_entrances = 'off'
|
||||
self.spawn_positions = 'off'
|
||||
self.shuffle_bosses = 'off'
|
||||
self.shuffle_grotto_entrances = False
|
||||
self.shuffle_dungeon_entrances = False
|
||||
self.shuffle_overworld_entrances = False
|
||||
self.owl_drops = False
|
||||
self.warp_songs = False
|
||||
self.spawn_positions = 'off'
|
||||
|
||||
# Fix spawn positions option
|
||||
new_sp = []
|
||||
@@ -197,6 +198,17 @@ class OOTWorld(World):
|
||||
if self.triforce_hunt:
|
||||
self.shuffle_ganon_bosskey = 'triforce'
|
||||
|
||||
# Force itempool to higher settings if it doesn't have enough hearts
|
||||
max_required_hearts = 3
|
||||
if self.bridge == 'hearts':
|
||||
max_required_hearts = max(max_required_hearts, self.bridge_hearts)
|
||||
if self.shuffle_ganon_bosskey == 'hearts':
|
||||
max_required_hearts = max(max_required_hearts, self.ganon_bosskey_hearts)
|
||||
if max_required_hearts > 3 and self.item_pool_value == 'minimal':
|
||||
self.item_pool_value = 'scarce'
|
||||
if max_required_hearts > 12 and self.item_pool_value == 'scarce':
|
||||
self.item_pool_value = 'balanced'
|
||||
|
||||
# If songs/keys locked to own world by settings, add them to local_items
|
||||
local_types = []
|
||||
if self.shuffle_song_items != 'any':
|
||||
@@ -283,8 +295,17 @@ class OOTWorld(World):
|
||||
self.shuffle_special_dungeon_entrances = self.shuffle_dungeon_entrances == 'all'
|
||||
self.shuffle_dungeon_entrances = self.shuffle_dungeon_entrances != 'off'
|
||||
self.ensure_tod_access = (self.shuffle_interior_entrances != 'off') or self.shuffle_overworld_entrances or self.spawn_positions
|
||||
self.entrance_shuffle = (self.shuffle_interior_entrances != 'off') or self.shuffle_grotto_entrances or self.shuffle_dungeon_entrances or \
|
||||
self.shuffle_overworld_entrances or self.owl_drops or self.warp_songs or self.spawn_positions
|
||||
self.entrance_shuffle = (
|
||||
self.shuffle_interior_entrances != 'off'
|
||||
or self.shuffle_bosses != 'off'
|
||||
or self.shuffle_dungeon_entrances
|
||||
or self.shuffle_special_dungeon_entrances
|
||||
or self.spawn_positions
|
||||
or self.shuffle_grotto_entrances
|
||||
or self.shuffle_overworld_entrances
|
||||
or self.owl_drops
|
||||
or self.warp_songs
|
||||
)
|
||||
self.disable_trade_revert = (self.shuffle_interior_entrances != 'off') or self.shuffle_overworld_entrances
|
||||
self.shuffle_special_interior_entrances = self.shuffle_interior_entrances == 'all'
|
||||
|
||||
@@ -317,13 +338,14 @@ class OOTWorld(World):
|
||||
|
||||
# Determine which dungeons are MQ. Not compatible with glitched logic.
|
||||
mq_dungeons = set()
|
||||
all_dungeons = [d['name'] for d in dungeon_table]
|
||||
if self.logic_rules != 'glitched':
|
||||
if self.mq_dungeons_mode == 'mq':
|
||||
mq_dungeons = dungeon_table.keys()
|
||||
mq_dungeons = all_dungeons
|
||||
elif self.mq_dungeons_mode == 'specific':
|
||||
mq_dungeons = self.mq_dungeons_specific
|
||||
elif self.mq_dungeons_mode == 'count':
|
||||
mq_dungeons = self.multiworld.random.sample(dungeon_table, self.mq_dungeons_count)
|
||||
mq_dungeons = self.multiworld.random.sample(all_dungeons, self.mq_dungeons_count)
|
||||
else:
|
||||
self.mq_dungeons_mode = 'count'
|
||||
self.mq_dungeons_count = 0
|
||||
@@ -441,6 +463,7 @@ class OOTWorld(World):
|
||||
new_region.scene = region['scene']
|
||||
if 'dungeon' in region:
|
||||
new_region.dungeon = region['dungeon']
|
||||
new_region.set_hint_data(region['dungeon'])
|
||||
if 'is_boss_room' in region:
|
||||
new_region.is_boss_room = region['is_boss_room']
|
||||
if 'hint' in region:
|
||||
@@ -842,10 +865,13 @@ class OOTWorld(World):
|
||||
impa = self.multiworld.get_location("Song from Impa", self.player)
|
||||
if self.shuffle_child_trade == 'skip_child_zelda':
|
||||
if impa.item is None:
|
||||
item_to_place = self.multiworld.random.choice(
|
||||
list(item for item in self.multiworld.itempool if item.player == self.player))
|
||||
candidate_items = list(item for item in self.multiworld.itempool if item.player == self.player)
|
||||
if candidate_items:
|
||||
item_to_place = self.multiworld.random.choice(candidate_items)
|
||||
self.multiworld.itempool.remove(item_to_place)
|
||||
else:
|
||||
item_to_place = self.create_item("Recovery Heart")
|
||||
impa.place_locked_item(item_to_place)
|
||||
self.multiworld.itempool.remove(item_to_place)
|
||||
# Give items to startinventory
|
||||
self.multiworld.push_precollected(impa.item)
|
||||
self.multiworld.push_precollected(self.create_item("Zeldas Letter"))
|
||||
@@ -1269,6 +1295,13 @@ def gather_locations(multiworld: MultiWorld,
|
||||
'HideoutSmallKey': 'shuffle_hideoutkeys',
|
||||
'GanonBossKey': 'shuffle_ganon_bosskey',
|
||||
}
|
||||
|
||||
# Special handling for atypical item types
|
||||
if item_type == 'HideoutSmallKey':
|
||||
dungeon = 'Thieves Hideout'
|
||||
elif item_type == 'GanonBossKey':
|
||||
dungeon = 'Ganons Castle'
|
||||
|
||||
if isinstance(players, int):
|
||||
players = {players}
|
||||
fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players}
|
||||
|
||||
@@ -135,10 +135,10 @@ def level_shuffle_factory(
|
||||
shuffle_horde_levels: bool,
|
||||
) -> Dict[int, Overcooked2GenericLevel]: # return <story_level_id, level>
|
||||
# Create a list of all valid levels for selection
|
||||
# (excludes tutorial, throne, kevin and sometimes horde levels)
|
||||
# (excludes tutorial, throne and sometimes horde/prep levels)
|
||||
pool = list()
|
||||
for dlc in Overcooked2Dlc:
|
||||
for level_id in range(dlc.start_level_id(), dlc.end_level_id()):
|
||||
for level_id in range(dlc.start_level_id, dlc.end_level_id):
|
||||
if level_id in dlc.excluded_levels():
|
||||
continue
|
||||
|
||||
@@ -165,11 +165,12 @@ def level_shuffle_factory(
|
||||
rng.shuffle(pool)
|
||||
|
||||
# Return the first 44 levels and assign those to each level
|
||||
for level_id in range(story.start_level_id(), story.end_level_id()):
|
||||
for level_id in range(story.start_level_id, story.end_level_id):
|
||||
if level_id not in story.excluded_levels():
|
||||
result[level_id] = pool[level_id-1]
|
||||
else:
|
||||
result[level_id] = Overcooked2GenericLevel(level_id) # This is just 6-6 right now
|
||||
elif level_id == 36:
|
||||
# Level 6-6 is exempt from shuffling
|
||||
result[level_id] = Overcooked2GenericLevel(level_id)
|
||||
|
||||
return result
|
||||
|
||||
@@ -2599,7 +2600,9 @@ level_logic = {
|
||||
{ # Exclusive
|
||||
|
||||
},
|
||||
horde_logic
|
||||
{ # Additive
|
||||
|
||||
},
|
||||
),
|
||||
( # 2-star
|
||||
{ # Exclusive
|
||||
|
||||
@@ -92,7 +92,7 @@ class StarsToWin(Range):
|
||||
display_name = "Stars to Win"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 66
|
||||
default = 60
|
||||
|
||||
|
||||
class StarThresholdScale(Range):
|
||||
@@ -101,7 +101,7 @@ class StarThresholdScale(Range):
|
||||
display_name = "Star Difficulty %"
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
default = 45
|
||||
default = 35
|
||||
|
||||
|
||||
overcooked_options = {
|
||||
|
||||
@@ -32,16 +32,16 @@ class Overcooked2Dlc(Enum):
|
||||
assert False
|
||||
|
||||
# inclusive
|
||||
@property
|
||||
def start_level_id(self) -> int:
|
||||
if self == Overcooked2Dlc.STORY:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
# exclusive
|
||||
@property
|
||||
def end_level_id(self) -> int:
|
||||
id = None
|
||||
if self == Overcooked2Dlc.STORY:
|
||||
id = 6*6 + 8 # world_count*level_count + kevin count
|
||||
id = 1 + 6*6 + 8 # tutorial + world_count*level_count + kevin count
|
||||
if self == Overcooked2Dlc.SURF_N_TURF:
|
||||
id = 3*4 + 1
|
||||
if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF:
|
||||
@@ -51,9 +51,9 @@ class Overcooked2Dlc(Enum):
|
||||
if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS:
|
||||
id = 3*4 + 3
|
||||
if self == Overcooked2Dlc.SEASONAL:
|
||||
id = 31
|
||||
id = 31 + 1
|
||||
|
||||
return self.start_level_id() + id
|
||||
return self.start_level_id + id
|
||||
|
||||
# Tutorial + Horde Levels + Endgame
|
||||
def excluded_levels(self) -> List[int]:
|
||||
|
||||
@@ -147,3 +147,26 @@ class Overcooked2Test(unittest.TestCase):
|
||||
|
||||
for item in ITEMS_TO_EXCLUDE_IF_NO_DLC:
|
||||
self.assertIn(item, item_table.keys())
|
||||
|
||||
def testLevelCounts(self):
|
||||
for dlc in Overcooked2Dlc:
|
||||
level_id_range = range(dlc.start_level_id, dlc.end_level_id)
|
||||
|
||||
for level_id in dlc.excluded_levels():
|
||||
self.assertIn(level_id, level_id_range, f"Excluded level {dlc.name} - {level_id} out of range")
|
||||
|
||||
for level_id in dlc.horde_levels():
|
||||
self.assertIn(level_id, level_id_range, f"Horde level {dlc.name} - {level_id} out of range")
|
||||
|
||||
for level_id in dlc.prep_levels():
|
||||
self.assertIn(level_id, level_id_range, f"Prep level {dlc.name} - {level_id} out of range")
|
||||
|
||||
for level_id in level_id_range:
|
||||
self.assertIn((dlc, level_id), level_id_to_shortname, "A valid level is not represented in level directory")
|
||||
|
||||
count = 0
|
||||
for (dlc_key, _) in level_id_to_shortname:
|
||||
if dlc == dlc_key:
|
||||
count += 1
|
||||
|
||||
self.assertEqual(count, len(level_id_range), f"Number of levels in {dlc.name} has discrepancy between level_id range and directory")
|
||||
|
||||
@@ -40,13 +40,11 @@ class PokemonRedBlueWorld(World):
|
||||
game = "Pokemon Red and Blue"
|
||||
option_definitions = pokemon_rb_options
|
||||
|
||||
data_version = 3
|
||||
data_version = 5
|
||||
required_client_version = (0, 3, 7)
|
||||
|
||||
topology_present = False
|
||||
|
||||
|
||||
|
||||
item_name_to_id = {name: data.id for name, data in item_table.items()}
|
||||
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"}
|
||||
item_name_groups = item_groups
|
||||
@@ -192,8 +190,9 @@ class PokemonRedBlueWorld(World):
|
||||
item = self.create_filler()
|
||||
else:
|
||||
item = self.create_item(location.original_item)
|
||||
combined_traps = self.multiworld.poison_trap_weight[self.player].value + self.multiworld.fire_trap_weight[self.player].value + self.multiworld.paralyze_trap_weight[self.player].value + self.multiworld.ice_trap_weight[self.player].value
|
||||
if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100)
|
||||
<= self.multiworld.trap_percentage[self.player].value):
|
||||
<= self.multiworld.trap_percentage[self.player].value and combined_traps != 0):
|
||||
item = self.create_item(self.select_trap())
|
||||
if location.event:
|
||||
self.multiworld.get_location(location.name, self.player).place_locked_item(item)
|
||||
@@ -319,7 +318,8 @@ class PokemonRedBlueWorld(World):
|
||||
spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n")
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value:
|
||||
combined_traps = self.multiworld.poison_trap_weight[self.player].value + self.multiworld.fire_trap_weight[self.player].value + self.multiworld.paralyze_trap_weight[self.player].value + self.multiworld.ice_trap_weight[self.player].value
|
||||
if self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value and combined_traps != 0:
|
||||
return self.select_trap()
|
||||
|
||||
return self.multiworld.random.choice([item for item in item_table if item_table[
|
||||
@@ -351,6 +351,7 @@ class PokemonRedBlueWorld(World):
|
||||
"elite_four_condition": self.multiworld.elite_four_condition[self.player].value,
|
||||
"victory_road_condition": self.multiworld.victory_road_condition[self.player].value,
|
||||
"viridian_gym_condition": self.multiworld.viridian_gym_condition[self.player].value,
|
||||
"cerulean_cave_condition": self.multiworld.cerulean_cave_condition[self.player].value,
|
||||
"free_fly_map": self.fly_map_code,
|
||||
"extra_badges": self.extra_badges,
|
||||
"type_chart": self.type_chart,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -357,7 +357,7 @@ location_data = [
|
||||
LocationData("Route 10 South", "Hidden Item Bush", "Max Ether", rom_addresses['Hidden_Item_Route_10_2'], Hidden(8), inclusion=hidden_items),
|
||||
LocationData("Rocket Hideout B1F", "Hidden Item Pot Plant", "PP Up", rom_addresses['Hidden_Item_Rocket_Hideout_B1F'], Hidden(9), inclusion=hidden_items),
|
||||
LocationData("Rocket Hideout B3F", "Hidden Item Near East Item", "Nugget", rom_addresses['Hidden_Item_Rocket_Hideout_B3F'], Hidden(10), inclusion=hidden_items),
|
||||
LocationData("Rocket Hideout B4F", "Hidden Item Behind Giovanni", "Super Potion", rom_addresses['Hidden_Item_Rocket_Hideout_B4F'], Hidden(11), inclusion=hidden_items),
|
||||
LocationData("Rocket Hideout B4F", "Hidden Item Behind Giovanni (Lift Key)", "Super Potion", rom_addresses['Hidden_Item_Rocket_Hideout_B4F'], Hidden(11), inclusion=hidden_items),
|
||||
LocationData("Pokemon Tower 5F", "Hidden Item Near West Staircase", "Elixir", rom_addresses['Hidden_Item_Pokemon_Tower_5F'], Hidden(12), inclusion=hidden_items),
|
||||
LocationData("Route 13", "Hidden Item Dead End Bush", "PP Up", rom_addresses['Hidden_Item_Route_13_1'], Hidden(13), inclusion=hidden_items),
|
||||
LocationData("Route 13", "Hidden Item Dead End By Water Corner", "Calcium", rom_addresses['Hidden_Item_Route_13_2'], Hidden(14), inclusion=hidden_items),
|
||||
@@ -506,7 +506,7 @@ location_data = [
|
||||
LocationData("Route 20 West", "Beauty 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_9_ITEM"], EventFlag(175), inclusion=trainersanity),
|
||||
LocationData("Route 20 West", "Jr. Trainer F 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_8_ITEM"], EventFlag(176), inclusion=trainersanity),
|
||||
LocationData("Route 20 West", "Beauty 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_7_ITEM"], EventFlag(177), inclusion=trainersanity),
|
||||
LocationData("Route 20 West", "Cooltrainer M", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_6_ITEM"], EventFlag(178), inclusion=trainersanity),
|
||||
LocationData("Route 20 West", "Bird Keeper", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_6_ITEM"], EventFlag(178), inclusion=trainersanity),
|
||||
LocationData("Route 20 West", "Swimmer 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_4_ITEM"], EventFlag(180), inclusion=trainersanity),
|
||||
LocationData("Route 20 West", "Jr. Trainer F 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_3_ITEM"], EventFlag(181), inclusion=trainersanity),
|
||||
LocationData("Route 20 East", "Beauty 3", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_2_ITEM"], EventFlag(182), inclusion=trainersanity),
|
||||
|
||||
@@ -518,6 +518,7 @@ class TrapWeight(Choice):
|
||||
option_low = 1
|
||||
option_medium = 3
|
||||
option_high = 5
|
||||
option_disabled = 0
|
||||
default = 3
|
||||
|
||||
|
||||
@@ -539,7 +540,6 @@ class ParalyzeTrapWeight(TrapWeight):
|
||||
class IceTrapWeight(TrapWeight):
|
||||
"""Weights for Ice Traps. These apply the Ice status to all your party members. Don't forget to buy Ice Heals!"""
|
||||
display_name = "Ice Trap Weight"
|
||||
option_disabled = 0
|
||||
default = 0
|
||||
|
||||
|
||||
@@ -605,4 +605,4 @@ pokemon_rb_options = {
|
||||
"paralyze_trap_weight": ParalyzeTrapWeight,
|
||||
"ice_trap_weight": IceTrapWeight,
|
||||
"death_link": DeathLink
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ def get_base_stat_total(mon):
|
||||
+ poke_data.pokemon_data[mon]["spc"])
|
||||
|
||||
|
||||
def randomize_pokemon(self, mon, mons_list, randomize_type):
|
||||
def randomize_pokemon(self, mon, mons_list, randomize_type, random):
|
||||
if randomize_type in [1, 3]:
|
||||
type_mons = [pokemon for pokemon in mons_list if any([poke_data.pokemon_data[mon][
|
||||
"type1"] in [self.local_poke_data[pokemon]["type1"], self.local_poke_data[pokemon]["type2"]],
|
||||
@@ -65,17 +65,17 @@ def randomize_pokemon(self, mon, mons_list, randomize_type):
|
||||
if randomize_type == 3:
|
||||
stat_base = get_base_stat_total(mon)
|
||||
type_mons.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base))
|
||||
mon = type_mons[round(self.multiworld.random.triangular(0, len(type_mons) - 1, 0))]
|
||||
mon = type_mons[round(random.triangular(0, len(type_mons) - 1, 0))]
|
||||
if randomize_type == 2:
|
||||
stat_base = get_base_stat_total(mon)
|
||||
mons_list.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base))
|
||||
mon = mons_list[round(self.multiworld.random.triangular(0, 50, 0))]
|
||||
mon = mons_list[round(random.triangular(0, 50, 0))]
|
||||
elif randomize_type == 4:
|
||||
mon = self.multiworld.random.choice(mons_list)
|
||||
mon = random.choice(mons_list)
|
||||
return mon
|
||||
|
||||
|
||||
def process_trainer_data(self, data):
|
||||
def process_trainer_data(self, data, random):
|
||||
mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon
|
||||
or self.multiworld.trainer_legendaries[self.player].value]
|
||||
address = rom_addresses["Trainer_Data"]
|
||||
@@ -94,14 +94,16 @@ def process_trainer_data(self, data):
|
||||
for i in range(1, 4):
|
||||
for l in ["A", "B", "C", "D", "E", "F", "G", "H"]:
|
||||
if rom_addresses[f"Rival_Starter{i}_{l}"] == address:
|
||||
mon = " ".join(self.multiworld.get_location(f"Pallet Town - Starter {i}", self.player).item.name.split()[1:])
|
||||
mon = " ".join(self.multiworld.get_location(f"Pallet Town - Starter {i}",
|
||||
self.player).item.name.split()[1:])
|
||||
if l in ["D", "E", "F", "G", "H"] and mon in poke_data.evolves_to:
|
||||
mon = poke_data.evolves_to[mon]
|
||||
if l in ["F", "G", "H"] and mon in poke_data.evolves_to:
|
||||
mon = poke_data.evolves_to[mon]
|
||||
if mon is None and self.multiworld.randomize_trainer_parties[self.player].value:
|
||||
mon = poke_data.id_to_mon[data[address]]
|
||||
mon = randomize_pokemon(self, mon, mons_list, self.multiworld.randomize_trainer_parties[self.player].value)
|
||||
mon = randomize_pokemon(self, mon, mons_list,
|
||||
self.multiworld.randomize_trainer_parties[self.player].value, random)
|
||||
if mon is not None:
|
||||
data[address] = poke_data.pokemon_data[mon]["id"]
|
||||
|
||||
@@ -154,10 +156,11 @@ def process_static_pokemon(self):
|
||||
location.place_locked_item(self.create_item(slot_type + " " + slot.original_item))
|
||||
else:
|
||||
mon = self.create_item(slot_type + " " +
|
||||
randomize_pokemon(self, slot.original_item, mons_list, randomize_type))
|
||||
randomize_pokemon(self, slot.original_item, mons_list, randomize_type,
|
||||
self.multiworld.random))
|
||||
while location.name == "Pokemon Tower 6F - Restless Soul" and mon in tower_6F_mons:
|
||||
mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list,
|
||||
randomize_type))
|
||||
randomize_type, self.multiworld.random))
|
||||
location.place_locked_item(mon)
|
||||
|
||||
for slot in starter_slots:
|
||||
@@ -168,7 +171,8 @@ def process_static_pokemon(self):
|
||||
location.place_locked_item(self.create_item(slot_type + " " + slot.original_item))
|
||||
else:
|
||||
location.place_locked_item(self.create_item(slot_type + " " +
|
||||
randomize_pokemon(self, slot.original_item, mons_list, randomize_type)))
|
||||
randomize_pokemon(self, slot.original_item, mons_list, randomize_type,
|
||||
self.multiworld.random)))
|
||||
|
||||
|
||||
def process_wild_pokemon(self):
|
||||
@@ -182,13 +186,14 @@ def process_wild_pokemon(self):
|
||||
self.multiworld.random.shuffle(encounter_slots)
|
||||
locations = []
|
||||
for slot in encounter_slots:
|
||||
mon = randomize_pokemon(self, slot.original_item, mons_list, self.multiworld.randomize_wild_pokemon[self.player].value)
|
||||
mon = randomize_pokemon(self, slot.original_item, mons_list,
|
||||
self.multiworld.randomize_wild_pokemon[self.player].value, self.multiworld.random)
|
||||
# if static Pokemon are not randomized, we make sure nothing on Pokemon Tower 6F is a Marowak
|
||||
# if static Pokemon are randomized we deal with that during static encounter randomization
|
||||
while (self.multiworld.randomize_static_pokemon[self.player].value == 0 and mon == "Marowak"
|
||||
and "Pokemon Tower 6F" in slot.name):
|
||||
# to account for the possibility that only one ground type Pokemon exists, match only stats for this fix
|
||||
mon = randomize_pokemon(self, slot.original_item, mons_list, 2)
|
||||
mon = randomize_pokemon(self, slot.original_item, mons_list, 2, self.multiworld.random)
|
||||
placed_mons[mon] += 1
|
||||
location = self.multiworld.get_location(slot.name, self.player)
|
||||
location.item = self.create_item(mon)
|
||||
@@ -326,16 +331,20 @@ def process_pokemon_data(self):
|
||||
else:
|
||||
mon_data["catch rate"] = max(self.multiworld.minimum_catch_rate[self.player], mon_data["catch rate"])
|
||||
|
||||
if mon in poke_data.evolves_from.keys() and mon_data["type1"] == local_poke_data[poke_data.evolves_from[mon]]["type1"] and mon_data["type2"] == local_poke_data[poke_data.evolves_from[mon]]["type2"]:
|
||||
mon_data["tms"] = local_poke_data[poke_data.evolves_from[mon]]["tms"]
|
||||
elif mon != "Mew":
|
||||
if mon != "Mew":
|
||||
tms_hms = poke_data.tm_moves + poke_data.hm_moves
|
||||
for flag, tm_move in enumerate(tms_hms):
|
||||
if (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 1) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 1):
|
||||
if ((mon in poke_data.evolves_from.keys() and mon_data["type1"] ==
|
||||
local_poke_data[poke_data.evolves_from[mon]]["type1"] and mon_data["type2"] ==
|
||||
local_poke_data[poke_data.evolves_from[mon]]["type2"]) and (
|
||||
(flag < 50 and self.multiworld.tm_compatibility[self.player].value in [1, 2]) or (
|
||||
flag >= 51 and self.multiworld.hm_compatibility[self.player].value in [1, 2]))):
|
||||
bit = 1 if local_poke_data[poke_data.evolves_from[mon]]["tms"][int(flag / 8)] & 1 << (flag % 8) else 0
|
||||
elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 1) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 1):
|
||||
type_match = poke_data.moves[tm_move]["type"] in [mon_data["type1"], mon_data["type2"]]
|
||||
bit = int(self.multiworld.random.randint(1, 100) < [[90, 50, 25], [100, 75, 25]][flag >= 50][0 if type_match else 1 if poke_data.moves[tm_move]["type"] == "Normal" else 2])
|
||||
elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 2) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 2):
|
||||
bit = [0, 1][self.multiworld.random.randint(0, 1)]
|
||||
bit = self.multiworld.random.randint(0, 1)
|
||||
elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 3) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 3):
|
||||
bit = 1
|
||||
else:
|
||||
@@ -390,6 +399,8 @@ def generate_output(self, output_directory: str):
|
||||
data[rom_addresses["Fossils_Needed_For_Second_Item"]] = (
|
||||
self.multiworld.second_fossil_check_condition[self.player].value)
|
||||
|
||||
data[rom_addresses["Option_Lose_Money"]] = int(not self.multiworld.lose_money_on_blackout[self.player].value)
|
||||
|
||||
if self.multiworld.extra_key_items[self.player].value:
|
||||
data[rom_addresses['Options']] |= 4
|
||||
data[rom_addresses["Option_Blind_Trainers"]] = round(self.multiworld.blind_trainers[self.player].value * 2.55)
|
||||
@@ -505,9 +516,9 @@ def generate_output(self, output_directory: str):
|
||||
inventory = ["Poke Ball", "Great Ball", "Ultra Ball"]
|
||||
if self.multiworld.better_shops[self.player].value == 2:
|
||||
inventory.append("Master Ball")
|
||||
inventory += ["Potion", "Super Potion", "Hyper Potion", "Max Potion", "Full Restore", "Antidote", "Awakening",
|
||||
"Burn Heal", "Ice Heal", "Paralyze Heal", "Full Heal", "Repel", "Super Repel", "Max Repel",
|
||||
"Escape Rope"]
|
||||
inventory += ["Potion", "Super Potion", "Hyper Potion", "Max Potion", "Full Restore", "Revive", "Antidote",
|
||||
"Awakening", "Burn Heal", "Ice Heal", "Paralyze Heal", "Full Heal", "Repel", "Super Repel",
|
||||
"Max Repel", "Escape Rope"]
|
||||
shop_data = bytearray([0xFE, len(inventory)])
|
||||
shop_data += bytearray([item_table[item].id - 172000000 for item in inventory])
|
||||
shop_data.append(0xFF)
|
||||
@@ -521,7 +532,7 @@ def generate_output(self, output_directory: str):
|
||||
if data[rom_addresses["Start_Inventory"] + item.code - 172000000] < 255:
|
||||
data[rom_addresses["Start_Inventory"] + item.code - 172000000] += 1
|
||||
|
||||
process_trainer_data(self, data)
|
||||
process_trainer_data(self, data, random)
|
||||
|
||||
mons = [mon["id"] for mon in poke_data.pokemon_data.values()]
|
||||
random.shuffle(mons)
|
||||
|
||||
@@ -113,27 +113,27 @@ rom_addresses = {
|
||||
"Event_Rocket_Thief": 0x196cc,
|
||||
"Option_Cerulean_Cave_Condition": 0x1986c,
|
||||
"Event_Stranded_Man": 0x19b1f,
|
||||
"Event_Rivals_Sister": 0x19ced,
|
||||
"Option_Pokemon_League_Badges": 0x19e0a,
|
||||
"Shop10": 0x19ee1,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a035,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a043,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a051,
|
||||
"Missable_Silph_Co_4F_Item_1": 0x1a0f9,
|
||||
"Missable_Silph_Co_4F_Item_2": 0x1a100,
|
||||
"Missable_Silph_Co_4F_Item_3": 0x1a107,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a25f,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a26d,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a27b,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a289,
|
||||
"Missable_Silph_Co_5F_Item_1": 0x1a361,
|
||||
"Missable_Silph_Co_5F_Item_2": 0x1a368,
|
||||
"Missable_Silph_Co_5F_Item_3": 0x1a36f,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a49f,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a4ad,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a4bb,
|
||||
"Missable_Silph_Co_6F_Item_1": 0x1a5dd,
|
||||
"Missable_Silph_Co_6F_Item_2": 0x1a5e4,
|
||||
"Event_Rivals_Sister": 0x19cf2,
|
||||
"Option_Pokemon_League_Badges": 0x19e0f,
|
||||
"Shop10": 0x19ee6,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a03a,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a048,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a056,
|
||||
"Missable_Silph_Co_4F_Item_1": 0x1a0fe,
|
||||
"Missable_Silph_Co_4F_Item_2": 0x1a105,
|
||||
"Missable_Silph_Co_4F_Item_3": 0x1a10c,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a264,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a272,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a280,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a28e,
|
||||
"Missable_Silph_Co_5F_Item_1": 0x1a366,
|
||||
"Missable_Silph_Co_5F_Item_2": 0x1a36d,
|
||||
"Missable_Silph_Co_5F_Item_3": 0x1a374,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a4a4,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a4b2,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a4c0,
|
||||
"Missable_Silph_Co_6F_Item_1": 0x1a5e2,
|
||||
"Missable_Silph_Co_6F_Item_2": 0x1a5e9,
|
||||
"Event_Free_Sample": 0x1cad6,
|
||||
"Starter1_F": 0x1cca2,
|
||||
"Starter2_F": 0x1cca6,
|
||||
@@ -838,46 +838,46 @@ rom_addresses = {
|
||||
"Ghost_Battle2": 0x60c5c,
|
||||
"Missable_Pokemon_Tower_6F_Item_1": 0x60cd7,
|
||||
"Missable_Pokemon_Tower_6F_Item_2": 0x60cde,
|
||||
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60ea6,
|
||||
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60eb4,
|
||||
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60ec2,
|
||||
"Gift_Aerodactyl": 0x610bc,
|
||||
"Gift_Omanyte": 0x610c0,
|
||||
"Gift_Kabuto": 0x610c4,
|
||||
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x611a7,
|
||||
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x611b5,
|
||||
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x611c3,
|
||||
"Missable_Viridian_Forest_Item_1": 0x6128a,
|
||||
"Missable_Viridian_Forest_Item_2": 0x61291,
|
||||
"Missable_Viridian_Forest_Item_3": 0x61298,
|
||||
"Starter2_M": 0x614ae,
|
||||
"Starter3_M": 0x614b6,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x6173c,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x6174a,
|
||||
"Event_SS_Anne_Captain": 0x61925,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a14,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a22,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a30,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a3e,
|
||||
"Missable_SS_Anne_1F_Item": 0x61b2a,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61bfb,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c09,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c17,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c25,
|
||||
"Missable_SS_Anne_2F_Item_1": 0x61d5f,
|
||||
"Missable_SS_Anne_2F_Item_2": 0x61d72,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e03,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e11,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e1f,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e2d,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e3b,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e49,
|
||||
"Missable_SS_Anne_B1F_Item_1": 0x61f61,
|
||||
"Missable_SS_Anne_B1F_Item_2": 0x61f68,
|
||||
"Missable_SS_Anne_B1F_Item_3": 0x61f6f,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x62330,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x6233e,
|
||||
"Event_Silph_Co_President": 0x62351,
|
||||
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60eb2,
|
||||
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60ec0,
|
||||
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60ece,
|
||||
"Gift_Aerodactyl": 0x610c8,
|
||||
"Gift_Omanyte": 0x610cc,
|
||||
"Gift_Kabuto": 0x610d0,
|
||||
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x611b3,
|
||||
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x611c1,
|
||||
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x611cf,
|
||||
"Missable_Viridian_Forest_Item_1": 0x61296,
|
||||
"Missable_Viridian_Forest_Item_2": 0x6129d,
|
||||
"Missable_Viridian_Forest_Item_3": 0x612a4,
|
||||
"Starter2_M": 0x614ba,
|
||||
"Starter3_M": 0x614c2,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x61748,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x61756,
|
||||
"Event_SS_Anne_Captain": 0x61931,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a20,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a2e,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a3c,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a4a,
|
||||
"Missable_SS_Anne_1F_Item": 0x61b36,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61c07,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c15,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c23,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c31,
|
||||
"Missable_SS_Anne_2F_Item_1": 0x61d6b,
|
||||
"Missable_SS_Anne_2F_Item_2": 0x61d7e,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e0f,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e1d,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e2b,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e39,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e47,
|
||||
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e55,
|
||||
"Missable_SS_Anne_B1F_Item_1": 0x61f6d,
|
||||
"Missable_SS_Anne_B1F_Item_2": 0x61f74,
|
||||
"Missable_SS_Anne_B1F_Item_3": 0x61f7b,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x6233c,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x6234a,
|
||||
"Event_Silph_Co_President": 0x6235d,
|
||||
"Ghost_Battle4": 0x708e1,
|
||||
"Badge_Viridian_Gym": 0x749f7,
|
||||
"Event_Viridian_Gym": 0x74a0b,
|
||||
|
||||
@@ -119,7 +119,8 @@ def set_rules(world, player):
|
||||
"Route 10 - Hidden Item Bush": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Rocket Hideout B3F - Hidden Item Near East Item": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Rocket Hideout B4F - Hidden Item Behind Giovanni": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Rocket Hideout B4F - Hidden Item Behind Giovanni (Lift Key)": lambda state:
|
||||
state.pokemon_rb_can_get_hidden_items(player) and state.has("Lift Key", player),
|
||||
"Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Route 13 - Hidden Item Dead End Bush": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from BaseClasses import MultiWorld, CollectionState
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
|
||||
|
||||
def get_upgrade_total(multiworld: MultiWorld, player: int) -> int:
|
||||
@@ -20,7 +20,7 @@ def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool
|
||||
|
||||
|
||||
def has_upgrades_percentage(state: CollectionState, player: int, percentage: float) -> bool:
|
||||
return has_upgrade_amount(state, player, get_upgrade_total(state.multiworld, player) * (round(percentage) // 100))
|
||||
return has_upgrade_amount(state, player, round(get_upgrade_total(state.multiworld, player) * (percentage / 100)))
|
||||
|
||||
|
||||
def has_movement_rune(state: CollectionState, player: int) -> bool:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import string
|
||||
from typing import Dict, List
|
||||
from .Items import RiskOfRainItem, item_table, item_pool_weights
|
||||
from .Locations import RiskOfRainLocation, item_pickups
|
||||
from .Rules import set_rules
|
||||
|
||||
from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial
|
||||
from .Options import ror2_options, ItemWeights
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, RegionType, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import RiskOfRainItem, item_pool_weights, item_table
|
||||
from .Locations import RiskOfRainLocation, item_pickups
|
||||
from .Options import ItemWeights, ror2_options
|
||||
from .Rules import set_rules
|
||||
|
||||
client_version = 1
|
||||
|
||||
@@ -36,7 +36,6 @@ class RiskOfRainWorld(World):
|
||||
location_name_to_id = item_pickups
|
||||
|
||||
data_version = 4
|
||||
forced_auto_forfeit = True
|
||||
web = RiskOfWeb()
|
||||
total_revivals: int
|
||||
|
||||
|
||||
@@ -873,7 +873,7 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
|
||||
|
||||
gates_len = len(gates)
|
||||
if gates_len >= 2:
|
||||
connect(world, player, names, 'Menu', LocationName.gate_1_boss_region,
|
||||
connect(world, player, names, LocationName.gate_0_region, LocationName.gate_1_boss_region,
|
||||
lambda state: (state.has(ItemName.emblem, player, gates[1].gate_emblem_count)))
|
||||
|
||||
if gate_bosses[1] == all_gate_bosses_table[king_boom_boo]:
|
||||
@@ -886,7 +886,7 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
|
||||
connect(world, player, names, LocationName.gate_1_region, shuffleable_regions[gates[1].gate_levels[i]])
|
||||
|
||||
if gates_len >= 3:
|
||||
connect(world, player, names, 'Menu', LocationName.gate_2_boss_region,
|
||||
connect(world, player, names, LocationName.gate_1_region, LocationName.gate_2_boss_region,
|
||||
lambda state: (state.has(ItemName.emblem, player, gates[2].gate_emblem_count)))
|
||||
|
||||
if gate_bosses[2] == all_gate_bosses_table[king_boom_boo]:
|
||||
@@ -899,7 +899,7 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
|
||||
connect(world, player, names, LocationName.gate_2_region, shuffleable_regions[gates[2].gate_levels[i]])
|
||||
|
||||
if gates_len >= 4:
|
||||
connect(world, player, names, 'Menu', LocationName.gate_3_boss_region,
|
||||
connect(world, player, names, LocationName.gate_2_region, LocationName.gate_3_boss_region,
|
||||
lambda state: (state.has(ItemName.emblem, player, gates[3].gate_emblem_count)))
|
||||
|
||||
if gate_bosses[3] == all_gate_bosses_table[king_boom_boo]:
|
||||
@@ -912,7 +912,7 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
|
||||
connect(world, player, names, LocationName.gate_3_region, shuffleable_regions[gates[3].gate_levels[i]])
|
||||
|
||||
if gates_len >= 5:
|
||||
connect(world, player, names, 'Menu', LocationName.gate_4_boss_region,
|
||||
connect(world, player, names, LocationName.gate_3_region, LocationName.gate_4_boss_region,
|
||||
lambda state: (state.has(ItemName.emblem, player, gates[4].gate_emblem_count)))
|
||||
|
||||
if gate_bosses[4] == all_gate_bosses_table[king_boom_boo]:
|
||||
@@ -925,7 +925,7 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
|
||||
connect(world, player, names, LocationName.gate_4_region, shuffleable_regions[gates[4].gate_levels[i]])
|
||||
|
||||
if gates_len >= 6:
|
||||
connect(world, player, names, 'Menu', LocationName.gate_5_boss_region,
|
||||
connect(world, player, names, LocationName.gate_4_region, LocationName.gate_5_boss_region,
|
||||
lambda state: (state.has(ItemName.emblem, player, gates[5].gate_emblem_count)))
|
||||
|
||||
if gate_bosses[5] == all_gate_bosses_table[king_boom_boo]:
|
||||
|
||||
@@ -24,7 +24,7 @@ class SA2BWeb(WebWorld):
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["RaspberrySpaceJam", "PoryGone"]
|
||||
["RaspberrySpaceJam", "PoryGone", "Entiss"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en]
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
- Quality of life mods
|
||||
- SA2 Volume Controls from: [SA2 Volume Controls Release Page] (https://gamebanana.com/mods/381193)
|
||||
|
||||
## Installation Procedures
|
||||
## Installation Procedures (Windows)
|
||||
|
||||
1. Install Sonic Adventure 2: Battle from Steam.
|
||||
|
||||
@@ -32,6 +32,26 @@
|
||||
|
||||
7. Launch the `SA2ModManager.exe` and make sure the SA2B_Archipelago mod is listed and enabled.
|
||||
|
||||
## Installation Procedures (Linux and Steam Deck)
|
||||
|
||||
1. Install Sonic Adventure 2: Battle from Steam.
|
||||
|
||||
2. In the properties for Sonic Adventure 2 on Steam, force the use of Proton Experimental as the compatibility tool.
|
||||
|
||||
3. Launch the game at least once without mods.
|
||||
|
||||
4. Install Sonic Adventure 2 Mod Loader as per its instructions. To launch it, add ``SA2ModManager.exe`` as a non-Steam game. In the properties on Steam for Sonic Adventure 2 Mod Loader, set it to use Proton as the compatibility tool.
|
||||
|
||||
5. The folder you installed the Sonic Adventure 2 Mod Loader into will now have a `/mods` directory.
|
||||
|
||||
6. Unpack the Archipelago Mod into this folder, so that `/mods/SA2B_Archipelago` is a valid path.
|
||||
|
||||
7. In the SA2B_Archipelago folder, copy the `APCpp.dll` file and paste it in the Sonic Adventure 2 install folder (where `SA2ModManager.exe` is).
|
||||
|
||||
8. Launch the `SA2ModManager.exe` from Steam and make sure the SA2B_Archipelago mod is listed and enabled.
|
||||
|
||||
Note: Ensure that you launch Sonic Adventure 2 from Steam directly on Linux, rather than launching using the `Save & Play` button in Sonic Adventure 2 Mod Loader.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Before launching the game, run the `SA2ModManager.exe`, select the SA2B_Archipelago mod, and hit the `Configure...` button.
|
||||
@@ -42,7 +62,7 @@
|
||||
|
||||
4. For the `Password` field under `AP Settings`, enter the server password if one exists, otherwise leave blank.
|
||||
|
||||
5. Click The `Save` button then hit `Save & Play` to launch the game.
|
||||
5. Click The `Save` button then hit `Save & Play` to launch the game. On Linux, launch Sonic Adventure 2 from Steam directly rather than using `Save & Play`.
|
||||
|
||||
6. Create a new save to connect to the MultiWorld game. A "Connected to Archipelago" message will appear if you sucessfully connect. If you close the game during play, you can reconnect to the MultiWorld game by selecting the same save file slot.
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class SMCollectionState(metaclass=AutoLogicRegister):
|
||||
# for unit tests where MultiWorld is instantiated before worlds
|
||||
if hasattr(parent, "state"):
|
||||
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff,
|
||||
parent.state.smbm[player].onlyBossLeft) for player in
|
||||
parent.state.smbm[player].onlyBossLeft, parent.state.smbm[player].lastAP) for player in
|
||||
parent.get_game_players("Super Metroid")}
|
||||
for player, group in parent.groups.items():
|
||||
if (group["game"] == "Super Metroid"):
|
||||
@@ -116,7 +116,7 @@ class SMWorld(World):
|
||||
Logic.factory('vanilla')
|
||||
|
||||
self.variaRando = VariaRandomizer(self.multiworld, get_base_rom_path(), self.player)
|
||||
self.multiworld.state.smbm[self.player] = SMBoolManager(self.player, self.variaRando.maxDifficulty)
|
||||
self.multiworld.state.smbm[self.player] = SMBoolManager(self.player, self.variaRando.maxDifficulty, lastAP = self.variaRando.args.startLocation)
|
||||
|
||||
# keeps Nothing items local so no player will ever pickup Nothing
|
||||
# doing so reduces contribution of this world to the Multiworld the more Nothing there is though
|
||||
@@ -628,6 +628,11 @@ class SMWorld(World):
|
||||
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
state.smbm[self.player].addItem(item.type)
|
||||
if (item.location != None and item.location.player == self.player):
|
||||
for entrance in self.multiworld.get_region(item.location.parent_region.name, self.player).entrances:
|
||||
if (entrance.parent_region.can_reach(state)):
|
||||
state.smbm[self.player].lastAP = entrance.parent_region.name
|
||||
break
|
||||
return super(SMWorld, self).collect(state, item)
|
||||
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
@@ -736,18 +741,34 @@ class SMLocation(Location):
|
||||
super(SMLocation, self).__init__(player, name, address, parent)
|
||||
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or (self.can_reach(state) and self.can_comeback(state, item))))
|
||||
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
|
||||
|
||||
def can_reach(self, state: CollectionState) -> bool:
|
||||
# self.access_rule computes faster on average, so placing it first for faster abort
|
||||
assert self.parent_region, "Can't reach location without region"
|
||||
return self.access_rule(state) and self.parent_region.can_reach(state) and self.can_comeback(state)
|
||||
|
||||
def can_comeback(self, state: CollectionState):
|
||||
# some specific early/late game checks
|
||||
if self.name == 'Bomb' or self.name == 'Mother Brain':
|
||||
return True
|
||||
|
||||
def can_comeback(self, state: CollectionState, item: Item):
|
||||
randoExec = state.multiworld.worlds[self.player].variaRando.randoExec
|
||||
n = 2 if GraphUtils.isStandardStart(randoExec.graphSettings.startAP) else 3
|
||||
# is early game
|
||||
if (len([loc for loc in state.locations_checked if loc.player == self.player]) <= n):
|
||||
return True
|
||||
|
||||
for key in locationsDict[self.name].AccessFrom.keys():
|
||||
if (randoExec.areaGraph.canAccessList( state.smbm[self.player],
|
||||
key,
|
||||
[randoExec.graphSettings.startAP, 'Landing Site'] if not GraphUtils.isStandardStart(randoExec.graphSettings.startAP) else ['Landing Site'],
|
||||
state.smbm[self.player].maxDiff)):
|
||||
smbm = state.smbm[self.player]
|
||||
if (randoExec.areaGraph.canAccess( smbm,
|
||||
smbm.lastAP,
|
||||
key,
|
||||
smbm.maxDiff,
|
||||
None)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class SMItem(Item):
|
||||
game = "Super Metroid"
|
||||
|
||||
@@ -367,22 +367,6 @@ class AccessGraph(object):
|
||||
#print("canAccess: {}".format(can))
|
||||
return can
|
||||
|
||||
# test access from an access point to a list of others, given an optional item
|
||||
def canAccessList(self, smbm, srcAccessPointName, destAccessPointNameList, maxDiff, item=None):
|
||||
if item is not None:
|
||||
smbm.addItem(item)
|
||||
#print("canAccess: item: {}, src: {}, dest: {}".format(item, srcAccessPointName, destAccessPointName))
|
||||
destAccessPointList = [self.accessPoints[destAccessPointName] for destAccessPointName in destAccessPointNameList]
|
||||
srcAccessPoint = self.accessPoints[srcAccessPointName]
|
||||
availAccessPoints = self.getAvailableAccessPoints(srcAccessPoint, smbm, maxDiff, item)
|
||||
can = any(ap in availAccessPoints for ap in destAccessPointList)
|
||||
# if not can:
|
||||
# self.log.debug("canAccess KO: avail = {}".format([ap.Name for ap in availAccessPoints.keys()]))
|
||||
if item is not None:
|
||||
smbm.removeItem(item)
|
||||
#print("canAccess: {}".format(can))
|
||||
return can
|
||||
|
||||
# returns a list of AccessPoint instances from srcAccessPointName to destAccessPointName
|
||||
# (not including source ap)
|
||||
# or None if no possible path
|
||||
|
||||
@@ -13,7 +13,7 @@ class SMBoolManager(object):
|
||||
items = ['ETank', 'Missile', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Reserve', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack', 'Nothing', 'NoEnergy', 'MotherBrain', 'Hyper'] + Bosses.Golden4()
|
||||
countItems = ['Missile', 'Super', 'PowerBomb', 'ETank', 'Reserve']
|
||||
|
||||
def __init__(self, player=0, maxDiff=sys.maxsize, onlyBossLeft = False):
|
||||
def __init__(self, player=0, maxDiff=sys.maxsize, onlyBossLeft = False, lastAP = 'Landing Site'):
|
||||
self._items = { }
|
||||
self._counts = { }
|
||||
|
||||
@@ -21,6 +21,8 @@ class SMBoolManager(object):
|
||||
self.maxDiff = maxDiff
|
||||
self.onlyBossLeft = onlyBossLeft
|
||||
|
||||
self.lastAP = lastAP
|
||||
|
||||
# cache related
|
||||
self.cacheKey = 0
|
||||
self.computeItemsPositions()
|
||||
|
||||
@@ -10,7 +10,7 @@ from rando.ItemLocContainer import ItemLocation
|
||||
# Holds settings not related to graph layout.
|
||||
class RandoSettings(object):
|
||||
def __init__(self, maxDiff, progSpeed, progDiff, qty, restrictions,
|
||||
superFun, runtimeLimit_s, plandoSettings, minDiff):
|
||||
superFun, runtimeLimit_s, PlandoOptions, minDiff):
|
||||
self.progSpeed = progSpeed.lower()
|
||||
self.progDiff = progDiff.lower()
|
||||
self.maxDiff = maxDiff
|
||||
@@ -20,7 +20,7 @@ class RandoSettings(object):
|
||||
self.runtimeLimit_s = runtimeLimit_s
|
||||
if self.runtimeLimit_s <= 0:
|
||||
self.runtimeLimit_s = sys.maxsize
|
||||
self.plandoSettings = plandoSettings
|
||||
self.PlandoOptions = PlandoOptions
|
||||
self.minDiff = minDiff
|
||||
|
||||
def getSuperFun(self):
|
||||
@@ -30,7 +30,7 @@ class RandoSettings(object):
|
||||
self.superFun = superFun[:]
|
||||
|
||||
def isPlandoRando(self):
|
||||
return self.plandoSettings is not None
|
||||
return self.PlandoOptions is not None
|
||||
|
||||
def getItemManager(self, smbm, nLocs):
|
||||
if not self.isPlandoRando():
|
||||
@@ -43,20 +43,20 @@ class RandoSettings(object):
|
||||
return None
|
||||
exclude = {'alreadyPlacedItems': defaultdict(int), 'forbiddenItems': []}
|
||||
# locsItems is a dict {'loc name': 'item type'}
|
||||
for locName,itemType in self.plandoSettings["locsItems"].items():
|
||||
for locName,itemType in self.PlandoOptions["locsItems"].items():
|
||||
if not any(loc.Name == locName for loc in locations):
|
||||
continue
|
||||
exclude['alreadyPlacedItems'][itemType] += 1
|
||||
exclude['alreadyPlacedItems']['total'] += 1
|
||||
|
||||
exclude['forbiddenItems'] = self.plandoSettings['forbiddenItems']
|
||||
exclude['forbiddenItems'] = self.PlandoOptions['forbiddenItems']
|
||||
|
||||
return exclude
|
||||
|
||||
def collectAlreadyPlacedItemLocations(self, container):
|
||||
if not self.isPlandoRando():
|
||||
return
|
||||
for locName,itemType in self.plandoSettings["locsItems"].items():
|
||||
for locName,itemType in self.PlandoOptions["locsItems"].items():
|
||||
if not any(loc.Name == locName for loc in container.unusedLocations):
|
||||
continue
|
||||
item = container.getNextItemInPool(itemType)
|
||||
|
||||
@@ -621,7 +621,7 @@ class VariaRandomizer:
|
||||
self.ctrlDict = { getattr(ctrl, button) : button for button in ctrlButton }
|
||||
args.moonWalk = ctrl.Moonwalk
|
||||
|
||||
plandoSettings = None
|
||||
PlandoOptions = None
|
||||
if args.plandoRando is not None:
|
||||
forceArg('progressionSpeed', 'speedrun', "'Progression Speed' forced to speedrun")
|
||||
progSpeed = 'speedrun'
|
||||
@@ -632,10 +632,10 @@ class VariaRandomizer:
|
||||
args.plandoRando = json.loads(args.plandoRando)
|
||||
RomPatches.ActivePatches[self.player] = args.plandoRando["patches"]
|
||||
DoorsManager.unserialize(args.plandoRando["doors"])
|
||||
plandoSettings = {"locsItems": args.plandoRando['locsItems'], "forbiddenItems": args.plandoRando['forbiddenItems']}
|
||||
PlandoOptions = {"locsItems": args.plandoRando['locsItems'], "forbiddenItems": args.plandoRando['forbiddenItems']}
|
||||
randoSettings = RandoSettings(self.maxDifficulty, progSpeed, progDiff, qty,
|
||||
restrictions, args.superFun, args.runtimeLimit_s,
|
||||
plandoSettings, minDifficulty)
|
||||
PlandoOptions, minDifficulty)
|
||||
|
||||
# print some parameters for jm's stats
|
||||
if args.jm == True:
|
||||
|
||||
Binary file not shown.
@@ -1,12 +1,12 @@
|
||||
import string
|
||||
|
||||
from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification, RegionType
|
||||
from .Items import item_table, item_pool, event_item_pairs
|
||||
from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, RegionType, Tutorial
|
||||
from .Items import event_item_pairs, item_pool, item_table
|
||||
from .Locations import location_table
|
||||
from .Options import spire_options
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from .Options import spire_options
|
||||
from ..AutoWorld import WebWorld, World
|
||||
|
||||
|
||||
class SpireWeb(WebWorld):
|
||||
@@ -36,8 +36,6 @@ class SpireWorld(World):
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = location_table
|
||||
|
||||
forced_auto_forfeit = True
|
||||
|
||||
def _get_slot_data(self):
|
||||
return {
|
||||
'seed': "".join(self.multiworld.slot_seeds[self.player].choice(string.ascii_letters) for i in range(16)),
|
||||
|
||||
@@ -31,8 +31,8 @@ class SwimRule(Choice):
|
||||
|
||||
|
||||
class EarlySeaglide(DefaultOnToggle):
|
||||
display_name = "Early Seaglide"
|
||||
"""Make sure 2 of the Seaglide Fragments are available in or near the Safe Shallows (Sphere 1 Locations)."""
|
||||
display_name = "Early Seaglide"
|
||||
|
||||
|
||||
class ItemPool(Choice):
|
||||
|
||||
@@ -152,7 +152,7 @@ def has_ultra_glide_fins(state: "CollectionState", player: int) -> bool:
|
||||
def get_max_swim_depth(state: "CollectionState", player: int) -> int:
|
||||
swim_rule: SwimRule = state.multiworld.swim_rule[player]
|
||||
depth: int = swim_rule.base_depth
|
||||
if swim_rule == swim_rule.consider_items:
|
||||
if swim_rule.consider_items:
|
||||
if has_seaglide(state, player):
|
||||
if has_ultra_high_capacity_tank(state, player):
|
||||
depth += 350 # It's about 800m. Give some room
|
||||
|
||||
@@ -42,12 +42,12 @@ class SubnauticaWorld(World):
|
||||
option_definitions = Options.options
|
||||
|
||||
data_version = 8
|
||||
required_client_version = (0, 3, 7)
|
||||
required_client_version = (0, 3, 8)
|
||||
|
||||
creatures_to_scan: List[str]
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if self.multiworld.early_seaglide:
|
||||
if self.multiworld.early_seaglide[self.player]:
|
||||
self.multiworld.local_early_items[self.player]["Seaglide Fragment"] = 2
|
||||
|
||||
scan_option: Options.AggressiveScanLogic = self.multiworld.creature_scan_logic[self.player]
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
|
||||
2. Start Subnautica. You should see a connect form with three text boxes in the top left of your main menu.
|
||||
|
||||
**If using Linux,** add ``WINEDLLOVERRIDES="winhttp=n,b" %command%`` to your Subnautica launch arguments on Steam. If you bought Subnautica elsewhere, you can either add it as a non-steam game and use those launch arguments or use winecfg to set the dll override.
|
||||
|
||||
## Connecting
|
||||
|
||||
Use the connect form in Subnautica's main menu to enter your connection information to connect to an Archipelago multiworld.
|
||||
@@ -33,18 +35,19 @@ Warning: Currently it is not checked whether a loaded savegame belongs to the mu
|
||||
## Console Commands
|
||||
|
||||
The mod adds the following console commands:
|
||||
- `say` sends the text following it to Archipelago as a chat message. ! is not an allowed character, use / instead.
|
||||
- `silent` toggles Archipelago chat messages appearing.
|
||||
- `say` sends the text following it to Archipelago as a chat message.
|
||||
- `!` is not an allowed character, use `/` in its place. For example, to use the [`!hint` command](/tutorial/Archipelago/commands/en#remote-commands), type `say /hint`.
|
||||
- `silent` toggles Archipelago messages appearing.
|
||||
- `tracker` rotates through the possible settings for the in-game tracker that displays the closest uncollected location.
|
||||
- `deathlink` toggles death link.
|
||||
|
||||
To enable the console in Subnautica, press `F3` and `F8`, then uncheck "Disable Console" in the top left. Press `F3` and `F8` again to close the menus.
|
||||
To enter a console command, press `Enter`.
|
||||
To enable the console in Subnautica, press `Shift+Enter`.
|
||||
|
||||
## Known Issues
|
||||
|
||||
- Do not attempt playing vanilla saves while the mod is installed, as the mod will override the scan information of the savegame.
|
||||
- When exiting to the main menu the mod's state is not properly reset. Loading a savegame from here will break various things.
|
||||
If you want to reload a save it is recommended you restart the game entirely.
|
||||
If you want to reload a save it is recommended you relaunch the game entirely.
|
||||
- Attempting to load a savegame containing no longer valid connection information without entering valid information on the main menu will hang on the loading screen.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -60,7 +60,7 @@ class ShuffleVaultBoxes(Toggle):
|
||||
|
||||
class ShufflePostgame(Toggle):
|
||||
"""Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal.
|
||||
Use this if you don't play with forfeit on victory. IMPORTANT NOTE: The possibility of your second
|
||||
Use this if you don't play with release on victory. IMPORTANT NOTE: The possibility of your second
|
||||
"Progressive Dots" showing up in the Caves is ignored, they will still be considered "postgame" in base settings."""
|
||||
display_name = "Shuffle Postgame"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
git+https://github.com/beauxq/zilliandomizer@a0fd0a1199c0234756b8637420c1e46434c687f0#egg=zilliandomizer==0.5.0
|
||||
git+https://github.com/beauxq/zilliandomizer@23d938daa5aaeaa3ec2255c50f2729b33a9e7f87#egg=zilliandomizer==0.5.1
|
||||
|
||||
Reference in New Issue
Block a user