Compare commits

..

1 Commits

Author SHA1 Message Date
NewSoupVi
eaf352daaf MultiServer: Correct tying of Context.groups 2025-01-11 22:01:05 +01:00
203 changed files with 2559 additions and 11699 deletions

View File

@@ -1,20 +1,8 @@
{ {
"include": [ "include": [
"../BizHawkClient.py", "type_check.py",
"../Patch.py",
"../test/general/test_groups.py",
"../test/general/test_helpers.py",
"../test/general/test_memory.py",
"../test/general/test_names.py",
"../test/multiworld/__init__.py",
"../test/multiworld/test_multiworlds.py",
"../test/netutils/__init__.py",
"../test/programs/__init__.py",
"../test/programs/test_multi_server.py",
"../test/utils/__init__.py",
"../test/webhost/test_descriptions.py",
"../worlds/AutoSNIClient.py", "../worlds/AutoSNIClient.py",
"type_check.py" "../Patch.py"
], ],
"exclude": [ "exclude": [

View File

@@ -26,7 +26,7 @@ jobs:
- name: "Install dependencies" - name: "Install dependencies"
run: | run: |
python -m pip install --upgrade pip pyright==1.1.392.post0 python -m pip install --upgrade pip pyright==1.1.358
python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes
- name: "pyright: strict check on specific files" - name: "pyright: strict check on specific files"

View File

@@ -869,40 +869,21 @@ class CollectionState():
def has(self, item: str, player: int, count: int = 1) -> bool: def has(self, item: str, player: int, count: int = 1) -> bool:
return self.prog_items[player][item] >= count return self.prog_items[player][item] >= count
# for loops are specifically used in all/any/count methods, instead of all()/any()/sum(), to avoid the overhead of
# creating and iterating generator instances. In `return all(player_prog_items[item] for item in items)`, the
# argument to all() would be a new generator instance, for example.
def has_all(self, items: Iterable[str], player: int) -> bool: def has_all(self, items: Iterable[str], player: int) -> bool:
"""Returns True if each item name of items is in state at least once.""" """Returns True if each item name of items is in state at least once."""
player_prog_items = self.prog_items[player] return all(self.prog_items[player][item] for item in items)
for item in items:
if not player_prog_items[item]:
return False
return True
def has_any(self, items: Iterable[str], player: int) -> bool: def has_any(self, items: Iterable[str], player: int) -> bool:
"""Returns True if at least one item name of items is in state at least once.""" """Returns True if at least one item name of items is in state at least once."""
player_prog_items = self.prog_items[player] return any(self.prog_items[player][item] for item in items)
for item in items:
if player_prog_items[item]:
return True
return False
def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool: def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if each item name is in the state at least as many times as specified.""" """Returns True if each item name is in the state at least as many times as specified."""
player_prog_items = self.prog_items[player] return all(self.prog_items[player][item] >= count for item, count in item_counts.items())
for item, count in item_counts.items():
if player_prog_items[item] < count:
return False
return True
def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool: def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if at least one item name is in the state at least as many times as specified.""" """Returns True if at least one item name is in the state at least as many times as specified."""
player_prog_items = self.prog_items[player] return any(self.prog_items[player][item] >= count for item, count in item_counts.items())
for item, count in item_counts.items():
if player_prog_items[item] >= count:
return True
return False
def count(self, item: str, player: int) -> int: def count(self, item: str, player: int) -> int:
return self.prog_items[player][item] return self.prog_items[player][item]
@@ -930,20 +911,11 @@ class CollectionState():
def count_from_list(self, items: Iterable[str], player: int) -> int: def count_from_list(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state.""" """Returns the cumulative count of items from a list present in state."""
player_prog_items = self.prog_items[player] return sum(self.prog_items[player][item_name] for item_name in items)
total = 0
for item_name in items:
total += player_prog_items[item_name]
return total
def count_from_list_unique(self, items: Iterable[str], player: int) -> int: def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item.""" """Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
player_prog_items = self.prog_items[player] return sum(self.prog_items[player][item_name] > 0 for item_name in items)
total = 0
for item_name in items:
if player_prog_items[item_name] > 0:
total += 1
return total
# item name group related # item name group related
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool: def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:

View File

@@ -31,7 +31,6 @@ import ssl
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
import kvui import kvui
import argparse
logger = logging.getLogger("Client") logger = logging.getLogger("Client")
@@ -460,13 +459,6 @@ class CommonContext:
await self.send_msgs([payload]) await self.send_msgs([payload])
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}]) await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])
async def check_locations(self, locations: typing.Collection[int]) -> set[int]:
"""Send new location checks to the server. Returns the set of actually new locations that were sent."""
locations = set(locations) & self.missing_locations
if locations:
await self.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(locations)}])
return locations
async def console_input(self) -> str: async def console_input(self) -> str:
if self.ui: if self.ui:
self.ui.focus_textinput() self.ui.focus_textinput()
@@ -709,16 +701,8 @@ class CommonContext:
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
def make_gui(self) -> "type[kvui.GameManager]": def make_gui(self) -> typing.Type["kvui.GameManager"]:
""" """To return the Kivy App class needed for run_gui so it can be overridden before being built"""
To return the Kivy `App` class needed for `run_gui` so it can be overridden before being built
Common changes are changing `base_title` to update the window title of the client and
updating `logging_pairs` to automatically make new tabs that can be filled with their respective logger.
ex. `logging_pairs.append(("Foo", "Bar"))`
will add a "Bar" tab which follows the logger returned from `logging.getLogger("Foo")`
"""
from kvui import GameManager from kvui import GameManager
class TextManager(GameManager): class TextManager(GameManager):
@@ -907,7 +891,6 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.disconnected_intentionally = True ctx.disconnected_intentionally = True
ctx.event_invalid_game() ctx.event_invalid_game()
elif 'IncompatibleVersion' in errors: elif 'IncompatibleVersion' in errors:
ctx.disconnected_intentionally = True
raise Exception('Server reported your client version as incompatible. ' raise Exception('Server reported your client version as incompatible. '
'This probably means you have to update.') 'This probably means you have to update.')
elif 'InvalidItemsHandling' in errors: elif 'InvalidItemsHandling' in errors:
@@ -1058,32 +1041,6 @@ def get_base_parser(description: typing.Optional[str] = None):
return parser return parser
def handle_url_arg(args: "argparse.Namespace",
parser: "typing.Optional[argparse.ArgumentParser]" = None) -> "argparse.Namespace":
"""
Parse the url arg "archipelago://name:pass@host:port" from launcher into correct launch args for CommonClient
If alternate data is required the urlparse response is saved back to args.url if valid
"""
if not args.url:
return args
url = urllib.parse.urlparse(args.url)
if url.scheme != "archipelago":
if not parser:
parser = get_base_parser()
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
return args
args.url = url
args.connect = url.netloc
if url.username:
args.name = urllib.parse.unquote(url.username)
if url.password:
args.password = urllib.parse.unquote(url.password)
return args
def run_as_textclient(*args): def run_as_textclient(*args):
class TextContext(CommonContext): class TextContext(CommonContext):
# Text Mode to use !hint and such with games that have no text entry # Text Mode to use !hint and such with games that have no text entry
@@ -1096,7 +1053,7 @@ def run_as_textclient(*args):
if password_requested and not self.password: if password_requested and not self.password:
await super(TextContext, self).server_auth(password_requested) await super(TextContext, self).server_auth(password_requested)
await self.get_username() await self.get_username()
await self.send_connect(game="") await self.send_connect()
def on_package(self, cmd: str, args: dict): def on_package(self, cmd: str, args: dict):
if cmd == "Connected": if cmd == "Connected":
@@ -1125,7 +1082,17 @@ def run_as_textclient(*args):
parser.add_argument("url", nargs="?", help="Archipelago connection url") parser.add_argument("url", nargs="?", help="Archipelago connection url")
args = parser.parse_args(args) args = parser.parse_args(args)
args = handle_url_arg(args, parser=parser) # handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
if args.url:
url = urllib.parse.urlparse(args.url)
if url.scheme == "archipelago":
args.connect = url.netloc
if url.username:
args.name = urllib.parse.unquote(url.username)
if url.password:
args.password = urllib.parse.unquote(url.password)
else:
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
# use colorama to display colored text highlighting on windows # use colorama to display colored text highlighting on windows
colorama.init() colorama.init()

28
Fill.py
View File

@@ -502,13 +502,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
# "priority fill" # "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking, single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority", one_item_per_player=True, allow_partial=True) name="Priority", one_item_per_player=False)
if prioritylocations:
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority Retry", one_item_per_player=False)
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations defaultlocations = prioritylocations + defaultlocations
@@ -577,26 +571,6 @@ def distribute_items_restrictive(multiworld: MultiWorld,
print_data = {"items": items_counter, "locations": locations_counter} print_data = {"items": items_counter, "locations": locations_counter}
logging.info(f"Per-Player counts: {print_data})") logging.info(f"Per-Player counts: {print_data})")
more_locations = locations_counter - items_counter
more_items = items_counter - locations_counter
for player in multiworld.player_ids:
if more_locations[player]:
logging.error(
f"Player {multiworld.get_player_name(player)} had {more_locations[player]} more locations than items.")
elif more_items[player]:
logging.warning(
f"Player {multiworld.get_player_name(player)} had {more_items[player]} more items than locations.")
if unfilled:
raise FillError(
f"Unable to fill all locations.\n" +
f"Unfilled locations({len(unfilled)}): {unfilled}"
)
else:
logging.warning(
f"Unable to place all items.\n" +
f"Unplaced items({len(unplaced)}): {unplaced}"
)
def flood_items(multiworld: MultiWorld) -> None: def flood_items(multiworld: MultiWorld) -> None:
# get items to distribute # get items to distribute

View File

@@ -42,9 +42,7 @@ def mystery_argparse():
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
parser.add_argument('--race', action='store_true', default=defaults.race) parser.add_argument('--race', action='store_true', default=defaults.race)
parser.add_argument('--meta_file_path', default=defaults.meta_file_path) parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level') parser.add_argument('--log_level', default='info', help='Sets log level')
parser.add_argument('--log_time', help="Add timestamps to STDOUT",
default=defaults.logtime, action='store_true')
parser.add_argument("--csv_output", action="store_true", parser.add_argument("--csv_output", action="store_true",
help="Output rolled player options to csv (made for async multiworld).") help="Output rolled player options to csv (made for async multiworld).")
parser.add_argument("--plando", default=defaults.plando_options, parser.add_argument("--plando", default=defaults.plando_options,
@@ -77,7 +75,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
seed = get_seed(args.seed) seed = get_seed(args.seed)
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time) Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
random.seed(seed) random.seed(seed)
seed_name = get_seed_name(random) seed_name = get_seed_name(random)
@@ -440,7 +438,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
if "linked_options" in weights: if "linked_options" in weights:
weights = roll_linked_options(weights) weights = roll_linked_options(weights)
valid_keys = {"triggers"} valid_keys = set()
if "triggers" in weights: if "triggers" in weights:
weights = roll_triggers(weights, weights["triggers"], valid_keys) weights = roll_triggers(weights, weights["triggers"], valid_keys)
@@ -499,22 +497,15 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
for option_key, option in world_type.options_dataclass.type_hints.items(): for option_key, option in world_type.options_dataclass.type_hints.items():
handle_option(ret, game_weights, option_key, option, plando_options) handle_option(ret, game_weights, option_key, option, plando_options)
valid_keys.add(option_key) valid_keys.add(option_key)
# TODO remove plando_items after moving it to the options system
valid_keys.add("plando_items")
if PlandoOptions.items in plando_options:
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
if ret.game == "A Link to the Past":
# TODO there are still more LTTP options not on the options system
valid_keys |= {"sprite_pool", "sprite", "random_sprite_on_event"}
roll_alttp_settings(ret, game_weights)
# log a warning for options within a game section that aren't determined as valid
for option_key in game_weights: for option_key in game_weights:
if option_key in valid_keys: if option_key in {"triggers", *valid_keys}:
continue continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers " logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers "
f"for player {ret.name}.") f"for player {ret.name}.")
if PlandoOptions.items in plando_options:
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
if ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights)
return ret return ret

View File

@@ -560,10 +560,6 @@ class LinksAwakeningContext(CommonContext):
while self.client.auth == None: while self.client.auth == None:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
# Just return if we're closing
if self.exit_event.is_set():
return
self.auth = self.client.auth self.auth = self.client.auth
await self.send_connect() await self.send_connect()

View File

@@ -33,15 +33,10 @@ WINDOW_MIN_HEIGHT = 525
WINDOW_MIN_WIDTH = 425 WINDOW_MIN_WIDTH = 425
class AdjusterWorld(object): class AdjusterWorld(object):
class AdjusterSubWorld(object):
def __init__(self, random):
self.random = random
def __init__(self, sprite_pool): def __init__(self, sprite_pool):
import random import random
self.sprite_pool = {1: sprite_pool} self.sprite_pool = {1: sprite_pool}
self.per_slot_randoms = {1: random} self.per_slot_randoms = {1: random}
self.worlds = {1: self.AdjusterSubWorld(random)}
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):

View File

@@ -148,8 +148,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
else: else:
multiworld.worlds[1].options.non_local_items.value = set() multiworld.worlds[1].options.non_local_items.value = set()
multiworld.worlds[1].options.local_items.value = set() multiworld.worlds[1].options.local_items.value = set()
AutoWorld.call_all(multiworld, "connect_entrances")
AutoWorld.call_all(multiworld, "generate_basic") AutoWorld.call_all(multiworld, "generate_basic")
# remove starting inventory from pool items. # remove starting inventory from pool items.

View File

@@ -28,11 +28,9 @@ ModuleUpdate.update()
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
import ssl import ssl
from NetUtils import ServerConnection
import colorama
import websockets import websockets
from websockets.extensions.permessage_deflate import PerMessageDeflate import colorama
try: try:
# ponyorm is a requirement for webhost, not default server, so may not be importable # ponyorm is a requirement for webhost, not default server, so may not be importable
from pony.orm.dbapiprovider import OperationalError from pony.orm.dbapiprovider import OperationalError
@@ -121,14 +119,13 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int:
class Client(Endpoint): class Client(Endpoint):
version = Version(0, 0, 0) version = Version(0, 0, 0)
tags: typing.List[str] tags: typing.List[str] = []
remote_items: bool remote_items: bool
remote_start_inventory: bool remote_start_inventory: bool
no_items: bool no_items: bool
no_locations: bool no_locations: bool
no_text: bool
def __init__(self, socket: "ServerConnection", ctx: Context) -> None: def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
super().__init__(socket) super().__init__(socket)
self.auth = False self.auth = False
self.team = None self.team = None
@@ -178,7 +175,6 @@ class Context:
"compatibility": int} "compatibility": int}
# team -> slot id -> list of clients authenticated to slot. # team -> slot id -> list of clients authenticated to slot.
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]] clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
endpoints: list[Client]
locations: LocationStore # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]] locations: LocationStore # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]] location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]]
hints_used: typing.Dict[typing.Tuple[int, int], int] hints_used: typing.Dict[typing.Tuple[int, int], int]
@@ -368,28 +364,18 @@ class Context:
return True return True
def broadcast_all(self, msgs: typing.List[dict]): def broadcast_all(self, msgs: typing.List[dict]):
msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs) msgs = self.dumper(msgs)
data = self.dumper(msgs) endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth)
endpoints = ( async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
endpoint
for endpoint in self.endpoints
if endpoint.auth and not (msg_is_text and endpoint.no_text)
)
async_start(self.broadcast_send_encoded_msgs(endpoints, data))
def broadcast_text_all(self, text: str, additional_arguments: dict = {}): def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
self.logger.info("Notice (all): %s" % text) self.logger.info("Notice (all): %s" % text)
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}]) self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
def broadcast_team(self, team: int, msgs: typing.List[dict]): def broadcast_team(self, team: int, msgs: typing.List[dict]):
msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs) msgs = self.dumper(msgs)
data = self.dumper(msgs) endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values()))
endpoints = ( async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
endpoint
for endpoint in itertools.chain.from_iterable(self.clients[team].values())
if not (msg_is_text and endpoint.no_text)
)
async_start(self.broadcast_send_encoded_msgs(endpoints, data))
def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]): def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]):
msgs = self.dumper(msgs) msgs = self.dumper(msgs)
@@ -403,13 +389,13 @@ class Context:
await on_client_disconnected(self, endpoint) await on_client_disconnected(self, endpoint)
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}): def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
if not client.auth or client.no_text: if not client.auth:
return return
self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}])) async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}): def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
if not client.auth or client.no_text: if not client.auth:
return return
async_start(self.send_msgs(client, async_start(self.send_msgs(client,
[{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments} [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}
@@ -757,24 +743,23 @@ class Context:
concerns[player].append(data) concerns[player].append(data)
if not hint.local and data not in concerns[hint.finding_player]: if not hint.local and data not in concerns[hint.finding_player]:
concerns[hint.finding_player].append(data) concerns[hint.finding_player].append(data)
# remember hints in all cases
# only remember hints that were not already found at the time of creation # since hints are bidirectional, finding player and receiving player,
if not hint.found: # we can check once if hint already exists
# since hints are bidirectional, finding player and receiving player, if hint not in self.hints[team, hint.finding_player]:
# we can check once if hint already exists self.hints[team, hint.finding_player].add(hint)
if hint not in self.hints[team, hint.finding_player]: new_hint_events.add(hint.finding_player)
self.hints[team, hint.finding_player].add(hint) for player in self.slot_set(hint.receiving_player):
new_hint_events.add(hint.finding_player) self.hints[team, player].add(hint)
for player in self.slot_set(hint.receiving_player): new_hint_events.add(player)
self.hints[team, player].add(hint)
new_hint_events.add(player)
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint))) self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
for slot in new_hint_events: for slot in new_hint_events:
self.on_new_hint(team, slot) self.on_new_hint(team, slot)
for slot, hint_data in concerns.items(): for slot, hint_data in concerns.items():
if recipients is None or slot in recipients: if recipients is None or slot in recipients:
clients = filter(lambda c: not c.no_text, self.clients[team].get(slot, [])) clients = self.clients[team].get(slot)
if not clients: if not clients:
continue continue
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)] client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)]
@@ -783,7 +768,7 @@ class Context:
def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]: def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]:
for hint in self.hints[team, finding_player]: for hint in self.hints[team, finding_player]:
if hint.location == seeked_location and hint.finding_player == finding_player: if hint.location == seeked_location:
return hint return hint
return None return None
@@ -833,7 +818,7 @@ def update_aliases(ctx: Context, team: int):
async_start(ctx.send_encoded_msgs(client, cmd)) async_start(ctx.send_encoded_msgs(client, cmd))
async def server(websocket: "ServerConnection", path: str = "/", ctx: Context = None) -> None: async def server(websocket, path: str = "/", ctx: Context = None):
client = Client(websocket, ctx) client = Client(websocket, ctx)
ctx.endpoints.append(client) ctx.endpoints.append(client)
@@ -924,10 +909,6 @@ async def on_client_joined(ctx: Context, client: Client):
"If your client supports it, " "If your client supports it, "
"you may have additional local commands you can list with /help.", "you may have additional local commands you can list with /help.",
{"type": "Tutorial"}) {"type": "Tutorial"})
if not any(isinstance(extension, PerMessageDeflate) for extension in client.socket.extensions):
ctx.notify_client(client, "Warning: your client does not support compressed websocket connections! "
"It may stop working in the future. If you are a player, please report this to the "
"client's developer.")
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
@@ -1078,37 +1059,21 @@ def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int], def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int],
count_activity: bool = True): count_activity: bool = True):
slot_locations = ctx.locations[slot]
new_locations = set(locations) - ctx.location_checks[team, slot] new_locations = set(locations) - ctx.location_checks[team, slot]
new_locations.intersection_update(slot_locations) # ignore location IDs unknown to this multidata new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata
if new_locations: if new_locations:
if count_activity: if count_activity:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
sortable: list[tuple[int, int, int, int]] = []
for location in new_locations: for location in new_locations:
# extract all fields to avoid runtime overhead in LocationStore item_id, target_player, flags = ctx.locations[slot][location]
item_id, target_player, flags = slot_locations[location]
# sort/group by receiver and item
sortable.append((target_player, item_id, location, flags))
info_texts: list[dict[str, typing.Any]] = []
for target_player, item_id, location, flags in sorted(sortable):
new_item = NetworkItem(item_id, location, slot, flags) new_item = NetworkItem(item_id, location, slot, flags)
send_items_to(ctx, team, target_player, new_item) send_items_to(ctx, team, target_player, new_item)
ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % ( ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id], team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id],
ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location])) ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location]))
if len(info_texts) >= 140: info_text = json_format_send_event(new_item, target_player)
# split into chunks that are close to compression window of 64K but not too big on the wire ctx.broadcast_team(team, [info_text])
# (roughly 1300-2600 bytes after compression depending on repetitiveness)
ctx.broadcast_team(team, info_texts)
info_texts.clear()
info_texts.append(json_format_send_event(new_item, target_player))
ctx.broadcast_team(team, info_texts)
del info_texts
del sortable
ctx.location_checks[team, slot] |= new_locations ctx.location_checks[team, slot] |= new_locations
send_new_items(ctx) send_new_items(ctx)
@@ -1135,7 +1100,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item] seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
for finding_player, location_id, item_id, receiving_player, item_flags \ for finding_player, location_id, item_id, receiving_player, item_flags \
in ctx.locations.find_item(slots, seeked_item_id): in ctx.locations.find_item(slots, seeked_item_id):
prev_hint = ctx.get_hint(team, finding_player, location_id) prev_hint = ctx.get_hint(team, slot, location_id)
if prev_hint: if prev_hint:
hints.append(prev_hint) hints.append(prev_hint)
else: else:
@@ -1821,9 +1786,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
ctx.clients[team][slot].append(client) ctx.clients[team][slot].append(client)
client.version = args['version'] client.version = args['version']
client.tags = args['tags'] client.tags = args['tags']
client.no_locations = "TextOnly" in client.tags or "Tracker" in client.tags client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
# set NoText for old PopTracker clients that predate the tag to save traffic
client.no_text = "NoText" in client.tags or ("PopTracker" in client.tags and client.version < (0, 5, 1))
connected_packet = { connected_packet = {
"cmd": "Connected", "cmd": "Connected",
"team": client.team, "slot": client.slot, "team": client.team, "slot": client.slot,
@@ -1896,9 +1859,6 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
client.tags = args["tags"] client.tags = args["tags"]
if set(old_tags) != set(client.tags): if set(old_tags) != set(client.tags):
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
client.no_text = "NoText" in client.tags or (
"PopTracker" in client.tags and client.version < (0, 5, 1)
)
ctx.broadcast_text_all( ctx.broadcast_text_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags " f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
f"from {old_tags} to {client.tags}.", f"from {old_tags} to {client.tags}.",
@@ -1927,8 +1887,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
for location in args["locations"]: for location in args["locations"]:
if type(location) is not int: if type(location) is not int:
await ctx.send_msgs(client, await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments", [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
"text": 'Locations has to be a list of integers',
"original_cmd": cmd}]) "original_cmd": cmd}])
return return
@@ -2031,7 +1990,6 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
args["cmd"] = "SetReply" args["cmd"] = "SetReply"
value = ctx.stored_data.get(args["key"], args.get("default", 0)) value = ctx.stored_data.get(args["key"], args.get("default", 0))
args["original_value"] = copy.copy(value) args["original_value"] = copy.copy(value)
args["slot"] = client.slot
for operation in args["operations"]: for operation in args["operations"]:
func = modify_functions[operation["operation"]] func = modify_functions[operation["operation"]]
value = func(value, operation["value"]) value = func(value, operation["value"])

View File

@@ -5,18 +5,17 @@ import enum
import warnings import warnings
from json import JSONEncoder, JSONDecoder from json import JSONEncoder, JSONDecoder
if typing.TYPE_CHECKING: import websockets
from websockets import WebSocketServerProtocol as ServerConnection
from Utils import ByValue, Version from Utils import ByValue, Version
class HintStatus(ByValue, enum.IntEnum): class HintStatus(ByValue, enum.IntEnum):
HINT_UNSPECIFIED = 0 HINT_FOUND = 0
HINT_UNSPECIFIED = 1
HINT_NO_PRIORITY = 10 HINT_NO_PRIORITY = 10
HINT_AVOID = 20 HINT_AVOID = 20
HINT_PRIORITY = 30 HINT_PRIORITY = 30
HINT_FOUND = 40
class JSONMessagePart(typing.TypedDict, total=False): class JSONMessagePart(typing.TypedDict, total=False):
@@ -152,7 +151,7 @@ decode = JSONDecoder(object_hook=_object_hook).decode
class Endpoint: class Endpoint:
socket: "ServerConnection" socket: websockets.WebSocketServerProtocol
def __init__(self, socket): def __init__(self, socket):
self.socket = socket self.socket = socket

View File

@@ -1,6 +1,7 @@
import tkinter as tk import tkinter as tk
import argparse import argparse
import logging import logging
import random
import os import os
import zipfile import zipfile
from itertools import chain from itertools import chain
@@ -196,6 +197,7 @@ def set_icon(window):
def adjust(args): def adjust(args):
# Create a fake multiworld and OOTWorld to use as a base # Create a fake multiworld and OOTWorld to use as a base
multiworld = MultiWorld(1) multiworld = MultiWorld(1)
multiworld.per_slot_randoms = {1: random}
ootworld = OOTWorld(multiworld, 1) ootworld = OOTWorld(multiworld, 1)
# Set options in the fake OOTWorld # Set options in the fake OOTWorld
for name, option in chain(cosmetic_options.items(), sfx_options.items()): for name, option in chain(cosmetic_options.items(), sfx_options.items()):

View File

@@ -137,7 +137,7 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
If this is False, the docstring is instead interpreted as plain text, and If this is False, the docstring is instead interpreted as plain text, and
displayed as-is on the WebHost with whitespace preserved. displayed as-is on the WebHost with whitespace preserved.
If this is None, it inherits the value of `WebWorld.rich_text_options_doc`. For If this is None, it inherits the value of `World.rich_text_options_doc`. For
backwards compatibility, this defaults to False, but worlds are encouraged to backwards compatibility, this defaults to False, but worlds are encouraged to
set it to True and use reStructuredText for their Option documentation. set it to True and use reStructuredText for their Option documentation.
@@ -689,9 +689,9 @@ class Range(NumericOption):
@classmethod @classmethod
def weighted_range(cls, text) -> Range: def weighted_range(cls, text) -> Range:
if text == "random-low": if text == "random-low":
return cls(cls.triangular(cls.range_start, cls.range_end, 0.0)) return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_start))
elif text == "random-high": elif text == "random-high":
return cls(cls.triangular(cls.range_start, cls.range_end, 1.0)) return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_end))
elif text == "random-middle": elif text == "random-middle":
return cls(cls.triangular(cls.range_start, cls.range_end)) return cls(cls.triangular(cls.range_start, cls.range_end))
elif text.startswith("random-range-"): elif text.startswith("random-range-"):
@@ -717,11 +717,11 @@ class Range(NumericOption):
f"{random_range[0]}-{random_range[1]} is outside allowed range " f"{random_range[0]}-{random_range[1]} is outside allowed range "
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}") f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
if text.startswith("random-range-low"): if text.startswith("random-range-low"):
return cls(cls.triangular(random_range[0], random_range[1], 0.0)) return cls(cls.triangular(random_range[0], random_range[1], random_range[0]))
elif text.startswith("random-range-middle"): elif text.startswith("random-range-middle"):
return cls(cls.triangular(random_range[0], random_range[1])) return cls(cls.triangular(random_range[0], random_range[1]))
elif text.startswith("random-range-high"): elif text.startswith("random-range-high"):
return cls(cls.triangular(random_range[0], random_range[1], 1.0)) return cls(cls.triangular(random_range[0], random_range[1], random_range[1]))
else: else:
return cls(random.randint(random_range[0], random_range[1])) return cls(random.randint(random_range[0], random_range[1]))
@@ -739,16 +739,8 @@ class Range(NumericOption):
return str(self.value) return str(self.value)
@staticmethod @staticmethod
def triangular(lower: int, end: int, tri: float = 0.5) -> int: def triangular(lower: int, end: int, tri: typing.Optional[int] = None) -> int:
""" return int(round(random.triangular(lower, end, tri), 0))
Integer triangular distribution for `lower` inclusive to `end` inclusive.
Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined.
"""
# Use the continuous range [lower, end + 1) to produce an integer result in [lower, end].
# random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even
# when a != b, so ensure the result is never more than `end`.
return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower))
class NamedRange(Range): class NamedRange(Range):
@@ -1582,7 +1574,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
} }
output.append(player_output) output.append(player_output)
for option_key, option in world.options_dataclass.type_hints.items(): for option_key, option in world.options_dataclass.type_hints.items():
if option.visibility == Visibility.none: if issubclass(Removed, option):
continue continue
display_name = getattr(option, "display_name", option_key) display_name = getattr(option, "display_name", option_key)
player_output[display_name] = getattr(world.options, option_key).current_option_name player_output[display_name] = getattr(world.options, option_key).current_option_name

View File

@@ -521,8 +521,8 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
def filter(self, record: logging.LogRecord) -> bool: def filter(self, record: logging.LogRecord) -> bool:
return self.condition(record) return self.condition(record)
file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False))) file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False)))
file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.getMessage())) file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.msg))
root_logger.addHandler(file_handler) root_logger.addHandler(file_handler)
if sys.stdout: if sys.stdout:
formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
@@ -940,7 +940,7 @@ def freeze_support() -> None:
def visualize_regions(root_region: Region, file_name: str, *, def visualize_regions(root_region: Region, file_name: str, *,
show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True, show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True,
linetype_ortho: bool = True, regions_to_highlight: set[Region] | None = None) -> None: linetype_ortho: bool = True) -> None:
"""Visualize the layout of a world as a PlantUML diagram. """Visualize the layout of a world as a PlantUML diagram.
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.) :param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
@@ -956,22 +956,16 @@ def visualize_regions(root_region: Region, file_name: str, *,
Items without ID will be shown in italics. Items without ID will be shown in italics.
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown. :param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines. :param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
:param regions_to_highlight: Regions that will be highlighted in green if they are reachable.
Example usage in World code: Example usage in World code:
from Utils import visualize_regions from Utils import visualize_regions
state = self.multiworld.get_all_state(False) visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
state.update_reachable_regions(self.player)
visualize_regions(self.get_region("Menu"), "my_world.puml", show_entrance_names=True,
regions_to_highlight=state.reachable_regions[self.player])
Example usage in Main code: Example usage in Main code:
from Utils import visualize_regions from Utils import visualize_regions
for player in multiworld.player_ids: for player in multiworld.player_ids:
visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml") visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml")
""" """
if regions_to_highlight is None:
regions_to_highlight = set()
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled" assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
from collections import deque from collections import deque
@@ -1024,7 +1018,7 @@ def visualize_regions(root_region: Region, file_name: str, *,
uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}") uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}")
def visualize_region(region: Region) -> None: def visualize_region(region: Region) -> None:
uml.append(f"class \"{fmt(region)}\" {'#00FF00' if region in regions_to_highlight else ''}") uml.append(f"class \"{fmt(region)}\"")
if show_locations: if show_locations:
visualize_locations(region) visualize_locations(region)
visualize_exits(region) visualize_exits(region)

View File

@@ -3,13 +3,13 @@ from typing import List, Tuple
from flask import Blueprint from flask import Blueprint
from ..models import Seed, Slot from ..models import Seed
api_endpoints = Blueprint('api', __name__, url_prefix="/api") api_endpoints = Blueprint('api', __name__, url_prefix="/api")
def get_players(seed: Seed) -> List[Tuple[str, str]]: def get_players(seed: Seed) -> List[Tuple[str, str]]:
return [(slot.player_name, slot.game) for slot in seed.slots.order_by(Slot.player_id)] return [(slot.player_name, slot.game) for slot in seed.slots]
from . import datapackage, generate, room, user # trigger registration from . import datapackage, generate, room, user # trigger registration

View File

@@ -30,4 +30,4 @@ def get_seeds():
"creation_time": seed.creation_time, "creation_time": seed.creation_time,
"players": get_players(seed.slots), "players": get_players(seed.slots),
}) })
return jsonify(response) return jsonify(response)

View File

@@ -22,7 +22,7 @@ players to rely upon each other to complete their game.
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
players to randomize any of the supported games, and send items between them. This allows players of different players to randomize any of the supported games, and send items between them. This allows players of different
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds. games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworld.
Here is a list of our [Supported Games](https://archipelago.gg/games). Here is a list of our [Supported Games](https://archipelago.gg/games).
## Can I generate a single-player game with Archipelago? ## Can I generate a single-player game with Archipelago?

View File

@@ -121,14 +121,6 @@ Response:
Expected Response Type: `HASH_RESPONSE` Expected Response Type: `HASH_RESPONSE`
- `MEMORY_SIZE`
Returns the size in bytes of the specified memory domain.
Expected Response Type: `MEMORY_SIZE_RESPONSE`
Additional Fields:
- `domain` (`string`): The name of the memory domain to check
- `GUARD` - `GUARD`
Checks a section of memory against `expected_data`. If the bytes starting Checks a section of memory against `expected_data`. If the bytes starting
at `address` do not match `expected_data`, the response will have `value` at `address` do not match `expected_data`, the response will have `value`
@@ -224,12 +216,6 @@ Response:
Additional Fields: Additional Fields:
- `value` (`string`): The returned hash - `value` (`string`): The returned hash
- `MEMORY_SIZE_RESPONSE`
Contains the size in bytes of the specified memory domain.
Additional Fields:
- `value` (`number`): The size of the domain in bytes
- `GUARD_RESPONSE` - `GUARD_RESPONSE`
The result of an attempted `GUARD` request. The result of an attempted `GUARD` request.
@@ -390,15 +376,6 @@ request_handlers = {
return res return res
end, end,
["MEMORY_SIZE"] = function (req)
local res = {}
res["type"] = "MEMORY_SIZE_RESPONSE"
res["value"] = memory.getmemorydomainsize(req["domain"])
return res
end,
["GUARD"] = function (req) ["GUARD"] = function (req)
local res = {} local res = {}
local expected_data = base64.decode(req["expected_data"]) local expected_data = base64.decode(req["expected_data"])
@@ -636,11 +613,9 @@ end)
if bizhawk_major < 2 or (bizhawk_major == 2 and bizhawk_minor < 7) then if bizhawk_major < 2 or (bizhawk_major == 2 and bizhawk_minor < 7) then
print("Must use BizHawk 2.7.0 or newer") print("Must use BizHawk 2.7.0 or newer")
elseif bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 9) then
print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.9.")
else else
if bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 10) then
print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.10.")
end
if emu.getsystemid() == "NULL" then if emu.getsystemid() == "NULL" then
print("No ROM is loaded. Please load a ROM.") print("No ROM is loaded. Please load a ROM.")
while emu.getsystemid() == "NULL" do while emu.getsystemid() == "NULL" do

View File

@@ -1816,7 +1816,7 @@ end
-- Main control handling: main loop and socket receive -- Main control handling: main loop and socket receive
function APreceive() function receive()
l, e = ootSocket:receive() l, e = ootSocket:receive()
-- Handle incoming message -- Handle incoming message
if e == 'closed' then if e == 'closed' then
@@ -1874,7 +1874,7 @@ function main()
end end
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
if (frame % 30 == 0) then if (frame % 30 == 0) then
APreceive() receive()
end end
elseif (curstate == STATE_UNINITIALIZED) then elseif (curstate == STATE_UNINITIALIZED) then
if (frame % 60 == 0) then if (frame % 60 == 0) then

View File

@@ -99,9 +99,6 @@
# Lingo # Lingo
/worlds/lingo/ @hatkirby /worlds/lingo/ @hatkirby
# Links Awakening DX
/worlds/ladx/ @threeandthreee
# Lufia II Ancient Cave # Lufia II Ancient Cave
/worlds/lufia2ac/ @el-u /worlds/lufia2ac/ @el-u
/worlds/lufia2ac/docs/ @wordfcuk @el-u /worlds/lufia2ac/docs/ @wordfcuk @el-u
@@ -239,6 +236,9 @@
# Final Fantasy (1) # Final Fantasy (1)
# /worlds/ff1/ # /worlds/ff1/
# Links Awakening DX
# /worlds/ladx/
# Ocarina of Time # Ocarina of Time
# /worlds/oot/ # /worlds/oot/

View File

@@ -370,13 +370,19 @@ target_group_lookup = bake_target_group_lookup(world, get_target_groups)
#### When to call `randomize_entrances` #### When to call `randomize_entrances`
The correct step for this is `World.connect_entrances`. The short answer is that you will almost always want to do ER in `pre_fill`. For more information why, continue reading.
Currently, you could theoretically do it as early as `World.create_regions` or as late as `pre_fill`. ER begins by collecting the entire item pool and then uses your access rules to try and prevent some kinds of failures.
However, there are upcoming changes to Item Plando and Generic Entrance Randomizer to make the two features work better This means 2 things about when you can call ER:
together. 1. You must supply your item pool before calling ER, or call ER before setting any rules which require items.
These changes necessitate that entrance randomization is done exactly in `World.connect_entrances`. 2. If you have rules dependent on anything other than items (e.g. `Entrance`s or events), you must set your rules
It is fine for your Entrances to be connected differently or not at all before this step. and create your events before you call ER if you want to guarantee a correct output.
If the conditions above are met, you could theoretically do ER as early as `create_regions`. However, plando is also
a consideration. Since item plando happens between `set_rules` and `pre_fill` and modifies the item pool, doing ER
in `pre_fill` is the only way to account for placements made by item plando, otherwise you risk impossible seeds or
generation failures. Obviously, if your world implements entrance plando, you will likely want to do that before ER as
well.
#### Informing your client about randomized entrances #### Informing your client about randomized entrances

View File

@@ -47,9 +47,6 @@ Packets are simple JSON lists in which any number of ordered network commands ca
An object can contain the "class" key, which will tell the content data type, such as "Version" in the following example. An object can contain the "class" key, which will tell the content data type, such as "Version" in the following example.
Websocket connections should support per-message compression. Uncompressed connections are deprecated and may stop
working in the future.
Example: Example:
```javascript ```javascript
[{"cmd": "RoomInfo", "version": {"major": 0, "minor": 1, "build": 3, "class": "Version"}, "tags": ["WebHost"], ... }] [{"cmd": "RoomInfo", "version": {"major": 0, "minor": 1, "build": 3, "class": "Version"}, "tags": ["WebHost"], ... }]
@@ -264,7 +261,6 @@ Sent to clients in response to a [Set](#Set) package if want_reply was set to tr
| key | str | The key that was updated. | | key | str | The key that was updated. |
| value | any | The new value for the key. | | value | any | The new value for the key. |
| original_value | any | The value the key had before it was updated. Not present on "_read" prefixed special keys. | | original_value | any | The value the key had before it was updated. Not present on "_read" prefixed special keys. |
| slot | int | The slot that originally sent the Set package causing this change. |
Additional arguments added to the [Set](#Set) package that triggered this [SetReply](#SetReply) will also be passed along. Additional arguments added to the [Set](#Set) package that triggered this [SetReply](#SetReply) will also be passed along.
@@ -363,11 +359,11 @@ An enumeration containing the possible hint states.
```python ```python
import enum import enum
class HintStatus(enum.IntEnum): class HintStatus(enum.IntEnum):
HINT_UNSPECIFIED = 0 # The receiving player has not specified any status HINT_FOUND = 0 # The location has been collected. Status cannot be changed once found.
HINT_UNSPECIFIED = 1 # The receiving player has not specified any status
HINT_NO_PRIORITY = 10 # The receiving player has specified that the item is unneeded HINT_NO_PRIORITY = 10 # The receiving player has specified that the item is unneeded
HINT_AVOID = 20 # The receiving player has specified that the item is detrimental HINT_AVOID = 20 # The receiving player has specified that the item is detrimental
HINT_PRIORITY = 30 # The receiving player has specified that the item is needed HINT_PRIORITY = 30 # The receiving player has specified that the item is needed
HINT_FOUND = 40 # The location has been collected. Status cannot be changed once found.
``` ```
- Hints for items with `ItemClassification.trap` default to `HINT_AVOID`. - Hints for items with `ItemClassification.trap` default to `HINT_AVOID`.
- Hints created with `LocationScouts`, `!hint_location`, or similar (hinting a location) default to `HINT_UNSPECIFIED`. - Hints created with `LocationScouts`, `!hint_location`, or similar (hinting a location) default to `HINT_UNSPECIFIED`.
@@ -533,9 +529,9 @@ In JSON this may look like:
{"item": 3, "location": 3, "player": 3, "flags": 0} {"item": 3, "location": 3, "player": 3, "flags": 0}
] ]
``` ```
`item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup> + 1, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use. `item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup> + 1, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use. `location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item `player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item
@@ -748,7 +744,6 @@ Tags are represented as a list of strings, the common client tags follow:
| HintGame | Indicates the client is a hint game, made to send hints instead of locations. Special join/leave message,¹ `game` is optional.² | | HintGame | Indicates the client is a hint game, made to send hints instead of locations. Special join/leave message,¹ `game` is optional.² |
| Tracker | Indicates the client is a tracker, made to track instead of sending locations. Special join/leave message,¹ `game` is optional.² | | Tracker | Indicates the client is a tracker, made to track instead of sending locations. Special join/leave message,¹ `game` is optional.² |
| TextOnly | Indicates the client is a basic client, made to chat instead of sending locations. Special join/leave message,¹ `game` is optional.² | | TextOnly | Indicates the client is a basic client, made to chat instead of sending locations. Special join/leave message,¹ `game` is optional.² |
| NoText | Indicates the client does not want to receive text messages, improving performance if not needed. |
¹: When connecting or disconnecting, the chat message shows e.g. "tracking".\ ¹: When connecting or disconnecting, the chat message shows e.g. "tracking".\
²: Allows `game` to be empty or null in [Connect](#connect). Game and version validation will then be skipped. ²: Allows `game` to be empty or null in [Connect](#connect). Game and version validation will then be skipped.

View File

@@ -95,7 +95,7 @@ user hovers over the yellow "(?)" icon, and included in the YAML templates gener
The WebHost can display Option documentation either as plain text with all whitespace preserved (other than the base The WebHost can display Option documentation either as plain text with all whitespace preserved (other than the base
indentation), or as HTML generated from the standard Python [reStructuredText] format. Although plain text is the indentation), or as HTML generated from the standard Python [reStructuredText] format. Although plain text is the
default for backwards compatibility, world authors are encouraged to write their Option documentation as default for backwards compatibility, world authors are encouraged to write their Option documentation as
reStructuredText and enable rich text rendering by setting `WebWorld.rich_text_options_doc = True`. reStructuredText and enable rich text rendering by setting `World.rich_text_options_doc = True`.
[reStructuredText]: https://docutils.sourceforge.io/rst.html [reStructuredText]: https://docutils.sourceforge.io/rst.html

View File

@@ -222,8 +222,8 @@ could also be progress in a research tree, or even something more abstract like
Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules, Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules,
and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1 and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1
letter or symbol). The ID needs to be unique across all locations within the game. letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs.
Locations and items can share IDs, and locations can share IDs with other games' locations. Locations and items can share IDs, so typically a game's locations and items start at the same ID.
World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved. World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved.
@@ -243,9 +243,7 @@ progression. Progression items will be assigned to locations with higher priorit
and satisfy progression balancing. and satisfy progression balancing.
The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they
will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol). will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol).
The ID thus also needs to be unique across all items with different names within the game.
Items and locations can share IDs, and items can share IDs with other games' items.
Other classifications include: Other classifications include:
@@ -492,9 +490,6 @@ In addition, the following methods can be implemented and are called in this ord
after this step. Locations cannot be moved to different regions after this step. after this step. Locations cannot be moved to different regions after this step.
* `set_rules(self)` * `set_rules(self)`
called to set access and item rules on locations and entrances. called to set access and item rules on locations and entrances.
* `connect_entrances(self)`
by the end of this step, all entrances must exist and be connected to their source and target regions.
Entrance randomization should be done here.
* `generate_basic(self)` * `generate_basic(self)`
player-specific randomization that does not affect logic can be done here. player-specific randomization that does not affect logic can be done here.
* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)` * `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)`
@@ -562,13 +557,17 @@ from .items import is_progression # this is just a dummy
def create_item(self, item: str) -> MyGameItem: def create_item(self, item: str) -> MyGameItem:
# this is called when AP wants to create an item by name (for plando) or when you call it from your own code # this is called when AP wants to create an item by name (for plando) or when you call it from your own code
classification = ItemClassification.progression if is_progression(item) else ItemClassification.filler classification = ItemClassification.progression if is_progression(item) else
return MyGameItem(item, classification, self.item_name_to_id[item], self.player) ItemClassification.filler
return MyGameItem(item, classification, self.item_name_to_id[item],
self.player)
def create_event(self, event: str) -> MyGameItem: def create_event(self, event: str) -> MyGameItem:
# while we are at it, we can also add a helper to create events # while we are at it, we can also add a helper to create events
return MyGameItem(event, ItemClassification.progression, None, self.player) return MyGameItem(event, True, None, self.player)
``` ```
#### create_items #### create_items
@@ -836,16 +835,14 @@ def generate_output(self, output_directory: str) -> None:
### Slot Data ### Slot Data
If a client or tracker needs to know information about the generated seed, a preferred method of transferring the data If the game client needs to know information about the generated seed, a preferred method of transferring the data
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning a `dict` with is through the slot data. This is filled with the `fill_slot_data` method of your world by returning
`str` keys that can be serialized with json. However, to not waste resources, it should be limited to data that is a `dict` with `str` keys that can be serialized with json.
absolutely necessary. Slot data is sent to your client once it has successfully But, to not waste resources, it should be limited to data that is absolutely necessary. Slot data is sent to your client
[connected](network%20protocol.md#connected). once it has successfully [connected](network%20protocol.md#connected).
If you need to know information about locations in your world, instead of propagating the slot data, it is preferable If you need to know information about locations in your world, instead of propagating the slot data, it is preferable
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. Adding to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most
item/location pairs is unnecessary since the AP server already retains and freely gives that information to clients common usage of slot data is sending option results that the client needs to be aware of.
that request it. The most common usage of slot data is sending option results that the client needs to be aware of.
```python ```python
def fill_slot_data(self) -> Dict[str, Any]: def fill_slot_data(self) -> Dict[str, Any]:

View File

@@ -378,14 +378,13 @@ def randomize_entrances(
and world.multiworld.has_beaten_game(er_state.collection_state, world.player): and world.multiworld.has_beaten_game(er_state.collection_state, world.player):
# ensure that we have enough locations to place our progression # ensure that we have enough locations to place our progression
accessible_location_count = 0 accessible_location_count = 0
prog_item_count = len([item for item in world.multiworld.itempool if item.advancement and item.player == world.player]) prog_item_count = sum(er_state.collection_state.prog_items[world.player].values())
# short-circuit location checking in this case # short-circuit location checking in this case
if prog_item_count == 0: if prog_item_count == 0:
return True return True
for region in er_state.placed_regions: for region in er_state.placed_regions:
for loc in region.locations: for loc in region.locations:
if not loc.item and loc.can_reach(er_state.collection_state): if loc.can_reach(er_state.collection_state):
# don't count locations with preplaced items
accessible_location_count += 1 accessible_location_count += 1
if accessible_location_count >= prog_item_count: if accessible_location_count >= prog_item_count:
perform_validity_check = False perform_validity_check = False

19
kvui.py
View File

@@ -26,10 +26,6 @@ import Utils
if Utils.is_frozen(): if Utils.is_frozen():
os.environ["KIVY_DATA_DIR"] = Utils.local_path("data") os.environ["KIVY_DATA_DIR"] = Utils.local_path("data")
import platformdirs
os.environ["KIVY_HOME"] = os.path.join(platformdirs.user_config_dir("Archipelago", False), "kivy")
os.makedirs(os.environ["KIVY_HOME"], exist_ok=True)
from kivy.config import Config from kivy.config import Config
Config.set("input", "mouse", "mouse,disable_multitouch") Config.set("input", "mouse", "mouse,disable_multitouch")
@@ -444,11 +440,8 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
if child.collide_point(*touch.pos): if child.collide_point(*touch.pos):
key = child.sort_key key = child.sort_key
if key == "status": if key == "status":
parent.hint_sorter = lambda element: status_sort_weights[element["status"]["hint"]["status"]] parent.hint_sorter = lambda element: element["status"]["hint"]["status"]
else: else: parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
parent.hint_sorter = lambda element: (
remove_between_brackets.sub("", element[key]["text"]).lower()
)
if key == parent.sort_key: if key == parent.sort_key:
# second click reverses order # second click reverses order
parent.reversed = not parent.reversed parent.reversed = not parent.reversed
@@ -832,13 +825,7 @@ status_colors: typing.Dict[HintStatus, str] = {
HintStatus.HINT_AVOID: "salmon", HintStatus.HINT_AVOID: "salmon",
HintStatus.HINT_PRIORITY: "plum", HintStatus.HINT_PRIORITY: "plum",
} }
status_sort_weights: dict[HintStatus, int] = {
HintStatus.HINT_FOUND: 0,
HintStatus.HINT_UNSPECIFIED: 1,
HintStatus.HINT_NO_PRIORITY: 2,
HintStatus.HINT_AVOID: 3,
HintStatus.HINT_PRIORITY: 4,
}
class HintLog(RecycleView): class HintLog(RecycleView):

View File

@@ -2,6 +2,3 @@
python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported
python_classes = Test python_classes = Test
python_functions = test python_functions = test
testpaths =
test
worlds

View File

@@ -109,7 +109,7 @@ class Group:
def get_type_hints(cls) -> Dict[str, Any]: def get_type_hints(cls) -> Dict[str, Any]:
"""Returns resolved type hints for the class""" """Returns resolved type hints for the class"""
if cls._type_cache is None: if cls._type_cache is None:
if not cls.__annotations__ or not isinstance(next(iter(cls.__annotations__.values())), str): if not isinstance(next(iter(cls.__annotations__.values())), str):
# non-str: assume already resolved # non-str: assume already resolved
cls._type_cache = cls.__annotations__ cls._type_cache = cls.__annotations__
else: else:
@@ -270,20 +270,15 @@ class Group:
# fetch class to avoid going through getattr # fetch class to avoid going through getattr
cls = self.__class__ cls = self.__class__
type_hints = cls.get_type_hints() type_hints = cls.get_type_hints()
entries = [e for e in self]
if not entries:
# write empty dict for empty Group with no instance values
cls._dump_value({}, f, indent=" " * level)
# validate group # validate group
for name in cls.__annotations__.keys(): for name in cls.__annotations__.keys():
assert hasattr(cls, name), f"{cls}.{name} is missing a default value" assert hasattr(cls, name), f"{cls}.{name} is missing a default value"
# dump ordered members # dump ordered members
for name in entries: for name in self:
attr = cast(object, getattr(self, name)) attr = cast(object, getattr(self, name))
attr_cls = type_hints[name] if name in type_hints else attr.__class__ attr_cls = type_hints[name] if name in type_hints else attr.__class__
attr_cls_origin = typing.get_origin(attr_cls) attr_cls_origin = typing.get_origin(attr_cls)
# resolve to first type for doc string while attr_cls_origin is Union: # resolve to first type for doc string
while attr_cls_origin is Union or attr_cls_origin is types.UnionType:
attr_cls = typing.get_args(attr_cls)[0] attr_cls = typing.get_args(attr_cls)[0]
attr_cls_origin = typing.get_origin(attr_cls) attr_cls_origin = typing.get_origin(attr_cls)
if attr_cls.__doc__ and attr_cls.__module__ != "builtins": if attr_cls.__doc__ and attr_cls.__module__ != "builtins":
@@ -683,8 +678,6 @@ class GeneratorOptions(Group):
race: Race = Race(0) race: Race = Race(0)
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts") plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
panic_method: PanicMethod = PanicMethod("swap") panic_method: PanicMethod = PanicMethod("swap")
loglevel: str = "info"
logtime: bool = False
class SNIOptions(Group): class SNIOptions(Group):
@@ -792,17 +785,7 @@ class Settings(Group):
if location: if location:
from Utils import parse_yaml from Utils import parse_yaml
with open(location, encoding="utf-8-sig") as f: with open(location, encoding="utf-8-sig") as f:
from yaml.error import MarkedYAMLError options = parse_yaml(f.read())
try:
options = parse_yaml(f.read())
except MarkedYAMLError as ex:
if ex.problem_mark:
f.seek(0)
lines = f.readlines()
problem_line = lines[ex.problem_mark.line]
error_line = " " * ex.problem_mark.column + "^"
raise Exception(f"{ex.context} {ex.problem}\n{problem_line}{error_line}")
raise ex
# TODO: detect if upgrade is required # TODO: detect if upgrade is required
# TODO: once we have a cache for _world_settings_name_cache, detect if any game section is missing # TODO: once we have a cache for _world_settings_name_cache, detect if any game section is missing
self.update(options or {}) self.update(options or {})

View File

@@ -18,15 +18,7 @@ def run_locations_benchmark():
class BenchmarkRunner: class BenchmarkRunner:
gen_steps: typing.Tuple[str, ...] = ( gen_steps: typing.Tuple[str, ...] = (
"generate_early", "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
"create_regions",
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
rule_iterations: int = 100_000 rule_iterations: int = 100_000
if sys.version_info >= (3, 9): if sys.version_info >= (3, 9):

View File

@@ -5,15 +5,7 @@ from BaseClasses import CollectionState, Item, ItemClassification, Location, Mul
from worlds import network_data_package from worlds import network_data_package
from worlds.AutoWorld import World, call_all from worlds.AutoWorld import World, call_all
gen_steps = ( gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
"generate_early",
"create_regions",
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
def setup_solo_multiworld( def setup_solo_multiworld(

View File

@@ -311,37 +311,6 @@ class TestRandomizeEntrances(unittest.TestCase):
self.assertEqual([], [exit_ for region in multiworld.get_regions() self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region]) for exit_ in region.exits if not exit_.connected_region])
def test_minimal_entrance_rando_with_collect_override(self):
"""
tests that entrance randomization can complete with minimal accessibility and unreachable exits
when the world defines a collect override that add extra values to prog_items
"""
multiworld = generate_test_multiworld()
multiworld.worlds[1].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
multiworld.completion_condition[1] = lambda state: state.can_reach("region24", player=1)
generate_disconnected_region_grid(multiworld, 5, 1)
prog_items = generate_items(10, 1, True)
multiworld.itempool += prog_items
filler_items = generate_items(15, 1, False)
multiworld.itempool += filler_items
e = multiworld.get_entrance("region1_right", 1)
set_rule(e, lambda state: False)
old_collect = multiworld.worlds[1].collect
def new_collect(state, item):
old_collect(state, item)
state.prog_items[item.player]["counter"] += 300
multiworld.worlds[1].collect = new_collect
randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
self.assertEqual([], [entrance for region in multiworld.get_regions()
for entrance in region.entrances if not entrance.parent_region])
self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region])
def test_restrictive_region_requirement_does_not_fail(self): def test_restrictive_region_requirement_does_not_fail(self):
multiworld = generate_test_multiworld() multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 2, 1) generate_disconnected_region_grid(multiworld, 2, 1)

View File

@@ -1,63 +0,0 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister, call_all, World
from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
def test_entrance_connection_steps(self):
"""Tests that Entrances are connected and not changed after connect_entrances."""
def get_entrance_name_to_source_and_target_dict(world: World):
return [
(entrance.name, entrance.parent_region, entrance.connected_region)
for entrance in world.get_entrances()
]
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
additional_steps = ("generate_basic", "pre_fill")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
original_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
self.assertTrue(
all(entrance[1] is not None and entrance[2] is not None for entrance in original_entrances),
f"{game_name} had unconnected entrances after connect_entrances"
)
for step in additional_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
step_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
self.assertEqual(
original_entrances, step_entrances, f"{game_name} modified entrances during {step}"
)
def test_all_state_before_connect_entrances(self):
"""Before connect_entrances, Entrance objects may be unconnected.
Thus, we test that get_all_state is performed with allow_partial_entrances if used before or during
connect_entrances."""
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, ())
original_get_all_state = multiworld.get_all_state
def patched_get_all_state(use_cache: bool, allow_partial_entrances: bool = False):
self.assertTrue(allow_partial_entrances, (
"Before the connect_entrances step finishes, other worlds might still have partial entrances. "
"As such, any call to get_all_state must use allow_partial_entrances = True."
))
return original_get_all_state(use_cache, allow_partial_entrances)
multiworld.get_all_state = patched_get_all_state
for step in gen_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)

View File

@@ -1,8 +1,6 @@
import unittest import unittest
from typing import Callable, Dict, Optional from typing import Callable, Dict, Optional
from typing_extensions import override
from BaseClasses import CollectionState, MultiWorld, Region from BaseClasses import CollectionState, MultiWorld, Region
@@ -10,7 +8,6 @@ class TestHelpers(unittest.TestCase):
multiworld: MultiWorld multiworld: MultiWorld
player: int = 1 player: int = 1
@override
def setUp(self) -> None: def setUp(self) -> None:
self.multiworld = MultiWorld(self.player) self.multiworld = MultiWorld(self.player)
self.multiworld.game[self.player] = "helper_test_game" self.multiworld.game[self.player] = "helper_test_game"
@@ -41,15 +38,15 @@ class TestHelpers(unittest.TestCase):
"TestRegion1": {"TestRegion2": "connection"}, "TestRegion1": {"TestRegion2": "connection"},
"TestRegion2": {"TestRegion1": None}, "TestRegion2": {"TestRegion1": None},
} }
reg_exit_set: Dict[str, set[str]] = { reg_exit_set: Dict[str, set[str]] = {
"TestRegion1": {"TestRegion3"} "TestRegion1": {"TestRegion3"}
} }
exit_rules: Dict[str, Callable[[CollectionState], bool]] = { exit_rules: Dict[str, Callable[[CollectionState], bool]] = {
"TestRegion1": lambda state: state.has("test_item", self.player) "TestRegion1": lambda state: state.has("test_item", self.player)
} }
self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions] self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions]
with self.subTest("Test Location Creation Helper"): with self.subTest("Test Location Creation Helper"):
@@ -76,7 +73,7 @@ class TestHelpers(unittest.TestCase):
entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}" entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
self.assertEqual(exit_rules[exit_reg], self.assertEqual(exit_rules[exit_reg],
self.multiworld.get_entrance(entrance_name, self.player).access_rule) self.multiworld.get_entrance(entrance_name, self.player).access_rule)
for region in reg_exit_set: for region in reg_exit_set:
current_region = self.multiworld.get_region(region, self.player) current_region = self.multiworld.get_region(region, self.player)
current_region.add_exits(reg_exit_set[region]) current_region.add_exits(reg_exit_set[region])

View File

@@ -39,7 +39,7 @@ class TestImplemented(unittest.TestCase):
"""Tests that if a world creates slot data, it's json serializable.""" """Tests that if a world creates slot data, it's json serializable."""
for game_name, world_type in AutoWorldRegister.world_types.items(): for game_name, world_type in AutoWorldRegister.world_types.items():
# has an await for generate_output which isn't being called # has an await for generate_output which isn't being called
if game_name in {"Ocarina of Time"}: if game_name in {"Ocarina of Time", "Zillion"}:
continue continue
multiworld = setup_solo_multiworld(world_type) multiworld = setup_solo_multiworld(world_type)
with self.subTest(game=game_name, seed=multiworld.seed): with self.subTest(game=game_name, seed=multiworld.seed):
@@ -117,12 +117,3 @@ class TestImplemented(unittest.TestCase):
f"\nUnexpectedly reachable locations in sphere {sphere_num}:" f"\nUnexpectedly reachable locations in sphere {sphere_num}:"
f"\n{reachable_only_with_explicit}") f"\n{reachable_only_with_explicit}")
self.fail("Unreachable") self.fail("Unreachable")
def test_no_items_or_locations_or_regions_submitted_in_init(self):
"""Test that worlds don't submit items/locations/regions to the multiworld in __init__"""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, ())
self.assertEqual(len(multiworld.itempool), 0)
self.assertEqual(len(multiworld.get_locations()), 0)
self.assertEqual(len(multiworld.get_regions()), 0)

View File

@@ -67,7 +67,7 @@ class TestBase(unittest.TestCase):
def test_itempool_not_modified(self): def test_itempool_not_modified(self):
"""Test that worlds don't modify the itempool after `create_items`""" """Test that worlds don't modify the itempool after `create_items`"""
gen_steps = ("generate_early", "create_regions", "create_items") gen_steps = ("generate_early", "create_regions", "create_items")
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill") additional_steps = ("set_rules", "generate_basic", "pre_fill")
excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3") excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
worlds_to_test = {game: world worlds_to_test = {game: world
for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games} for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
@@ -84,7 +84,7 @@ class TestBase(unittest.TestCase):
def test_locality_not_modified(self): def test_locality_not_modified(self):
"""Test that worlds don't modify the locality of items after duplicates are resolved""" """Test that worlds don't modify the locality of items after duplicates are resolved"""
gen_steps = ("generate_early", "create_regions", "create_items") gen_steps = ("generate_early", "create_regions", "create_items")
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill") additional_steps = ("set_rules", "generate_basic", "pre_fill")
worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()} worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()}
for game_name, world_type in worlds_to_test.items(): for game_name, world_type in worlds_to_test.items():
with self.subTest("Game", game=game_name): with self.subTest("Game", game=game_name):

View File

@@ -45,12 +45,6 @@ class TestBase(unittest.TestCase):
self.assertEqual(location_count, len(multiworld.get_locations()), self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation") f"{game_name} modified locations count during rule creation")
call_all(multiworld, "connect_entrances")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during rule creation")
self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation")
call_all(multiworld, "generate_basic") call_all(multiworld, "generate_basic")
self.assertEqual(region_count, len(multiworld.get_regions()), self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during generate_basic") f"{game_name} modified region count during generate_basic")

View File

@@ -1,21 +1,16 @@
import unittest import unittest
from BaseClasses import MultiWorld
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
from . import setup_solo_multiworld from . import setup_solo_multiworld
class TestWorldMemory(unittest.TestCase): class TestWorldMemory(unittest.TestCase):
def test_leak(self) -> None: def test_leak(self):
"""Tests that worlds don't leak references to MultiWorld or themselves with default options.""" """Tests that worlds don't leak references to MultiWorld or themselves with default options."""
import gc import gc
import weakref import weakref
refs: dict[str, weakref.ReferenceType[MultiWorld]] = {}
for game_name, world_type in AutoWorldRegister.world_types.items(): for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game creation", game_name=game_name): with self.subTest("Game", game_name=game_name):
weak = weakref.ref(setup_solo_multiworld(world_type)) weak = weakref.ref(setup_solo_multiworld(world_type))
refs[game_name] = weak gc.collect()
gc.collect()
for game_name, weak in refs.items():
with self.subTest("Game cleanup", game_name=game_name):
self.assertFalse(weak(), "World leaked a reference") self.assertFalse(weak(), "World leaked a reference")

View File

@@ -3,7 +3,7 @@ from worlds.AutoWorld import AutoWorldRegister
class TestNames(unittest.TestCase): class TestNames(unittest.TestCase):
def test_item_names_format(self) -> None: def test_item_names_format(self):
"""Item names must not be all numeric in order to differentiate between ID and name in !hint""" """Item names must not be all numeric in order to differentiate between ID and name in !hint"""
for gamename, world_type in AutoWorldRegister.world_types.items(): for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename): with self.subTest(game=gamename):
@@ -11,7 +11,7 @@ class TestNames(unittest.TestCase):
self.assertFalse(item_name.isnumeric(), self.assertFalse(item_name.isnumeric(),
f"Item name \"{item_name}\" is invalid. It must not be numeric.") f"Item name \"{item_name}\" is invalid. It must not be numeric.")
def test_location_name_format(self) -> None: def test_location_name_format(self):
"""Location names must not be all numeric in order to differentiate between ID and name in !hint_location""" """Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
for gamename, world_type in AutoWorldRegister.world_types.items(): for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename): with self.subTest(game=gamename):

View File

@@ -2,11 +2,11 @@ import unittest
from BaseClasses import CollectionState from BaseClasses import CollectionState
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
from . import setup_solo_multiworld, gen_steps from . import setup_solo_multiworld
class TestBase(unittest.TestCase): class TestBase(unittest.TestCase):
gen_steps = gen_steps gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
default_settings_unreachable_regions = { default_settings_unreachable_regions = {
"A Link to the Past": { "A Link to the Past": {

View File

@@ -1,29 +0,0 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
gen_steps = (
"generate_early",
"create_regions",
)
test_steps = (
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
def test_all_state_is_available(self):
"""Ensure all_state can be created at certain steps."""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, self.gen_steps)
for step in self.test_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
self.assertTrue(multiworld.get_all_state(False, True))

View File

@@ -378,10 +378,6 @@ class World(metaclass=AutoWorldRegister):
"""Method for setting the rules on the World's regions and locations.""" """Method for setting the rules on the World's regions and locations."""
pass pass
def connect_entrances(self) -> None:
"""Method to finalize the source and target regions of the World's entrances"""
pass
def generate_basic(self) -> None: def generate_basic(self) -> None:
""" """
Useful for randomizing things that don't affect logic but are better to be determined before the output stage. Useful for randomizing things that don't affect logic but are better to be determined before the output stage.

View File

@@ -87,7 +87,7 @@ class Component:
processes = weakref.WeakSet() processes = weakref.WeakSet()
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None: def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
global processes global processes
import multiprocessing import multiprocessing
process = multiprocessing.Process(target=func, name=name, args=args) process = multiprocessing.Process(target=func, name=name, args=args)
@@ -95,14 +95,6 @@ def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str,
processes.add(process) processes.add(process)
def launch(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
from Utils import is_kivy_running
if is_kivy_running():
launch_subprocess(func, name, args)
else:
func(*args)
class SuffixIdentifier: class SuffixIdentifier:
suffixes: Iterable[str] suffixes: Iterable[str]
@@ -119,7 +111,7 @@ class SuffixIdentifier:
def launch_textclient(*args): def launch_textclient(*args):
import CommonClient import CommonClient
launch(CommonClient.run_as_textclient, name="TextClient", args=args) launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]: def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:

View File

@@ -55,7 +55,6 @@ async def lock(ctx) -> None
async def unlock(ctx) -> None async def unlock(ctx) -> None
async def get_hash(ctx) -> str async def get_hash(ctx) -> str
async def get_memory_size(ctx, domain: str) -> int
async def get_system(ctx) -> str async def get_system(ctx) -> str
async def get_cores(ctx) -> dict[str, str] async def get_cores(ctx) -> dict[str, str]
async def ping(ctx) -> None async def ping(ctx) -> None
@@ -169,10 +168,9 @@ select dialog and they will be associated with BizHawkClient. This does not affe
associate the file extension with Archipelago. associate the file extension with Archipelago.
`validate_rom` is called to figure out whether a given ROM belongs to your client. It will only be called when a ROM is `validate_rom` is called to figure out whether a given ROM belongs to your client. It will only be called when a ROM is
running on a system you specified in your `system` class variable. Take extra care here, because your code will run running on a system you specified in your `system` class variable. In most cases, that will be a single system and you
against ROMs that you have no control over. If you're reading an address deep in ROM, you might want to check the size can be sure that you're not about to try to read from nonexistent domains or out of bounds. If you decide to claim this
of ROM before you attempt to read it using `get_memory_size`. If you decide to claim this ROM as yours, this is where ROM as yours, this is where you should do setup for things like `items_handling`.
you should do setup for things like `items_handling`.
`game_watcher` is the "main loop" of your client where you should be checking memory and sending new items to the ROM. `game_watcher` is the "main loop" of your client where you should be checking memory and sending new items to the ROM.
`BizHawkClient` will make sure that your `game_watcher` only runs when your client has validated the ROM, and will do `BizHawkClient` will make sure that your `game_watcher` only runs when your client has validated the ROM, and will do
@@ -270,8 +268,6 @@ server connection before trying to interact with it.
- By default, the player will be asked to provide their slot name after connecting to the server and validating, and - By default, the player will be asked to provide their slot name after connecting to the server and validating, and
that input will be used to authenticate with the `Connect` command. You can override `set_auth` in your own client to that input will be used to authenticate with the `Connect` command. You can override `set_auth` in your own client to
set it automatically based on data in the ROM or on your client instance. set it automatically based on data in the ROM or on your client instance.
- Use `get_memory_size` inside `validate_rom` if you need to read at large addresses, in case some other game has a
smaller ROM size.
- You can override `on_package` in your client to watch raw packages, but don't forget you also have access to a - You can override `on_package` in your client to watch raw packages, but don't forget you also have access to a
subclass of `CommonContext` and its API. subclass of `CommonContext` and its API.
- You can import `BizHawkClientContext` for type hints using `typing.TYPE_CHECKING`. Importing it without conditions at - You can import `BizHawkClientContext` for type hints using `typing.TYPE_CHECKING`. Importing it without conditions at

View File

@@ -10,7 +10,7 @@ import base64
import enum import enum
import json import json
import sys import sys
from typing import Any, Sequence import typing
BIZHAWK_SOCKET_PORT_RANGE_START = 43055 BIZHAWK_SOCKET_PORT_RANGE_START = 43055
@@ -44,10 +44,10 @@ class SyncError(Exception):
class BizHawkContext: class BizHawkContext:
streams: tuple[asyncio.StreamReader, asyncio.StreamWriter] | None streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
connection_status: ConnectionStatus connection_status: ConnectionStatus
_lock: asyncio.Lock _lock: asyncio.Lock
_port: int | None _port: typing.Optional[int]
def __init__(self) -> None: def __init__(self) -> None:
self.streams = None self.streams = None
@@ -122,12 +122,12 @@ async def get_script_version(ctx: BizHawkContext) -> int:
return int(await ctx._send_message("VERSION")) return int(await ctx._send_message("VERSION"))
async def send_requests(ctx: BizHawkContext, req_list: list[dict[str, Any]]) -> list[dict[str, Any]]: async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]:
"""Sends a list of requests to the BizHawk connector and returns their responses. """Sends a list of requests to the BizHawk connector and returns their responses.
It's likely you want to use the wrapper functions instead of this.""" It's likely you want to use the wrapper functions instead of this."""
responses = json.loads(await ctx._send_message(json.dumps(req_list))) responses = json.loads(await ctx._send_message(json.dumps(req_list)))
errors: list[ConnectorError] = [] errors: typing.List[ConnectorError] = []
for response in responses: for response in responses:
if response["type"] == "ERROR": if response["type"] == "ERROR":
@@ -151,7 +151,7 @@ async def ping(ctx: BizHawkContext) -> None:
async def get_hash(ctx: BizHawkContext) -> str: async def get_hash(ctx: BizHawkContext) -> str:
"""Gets the hash value of the currently loaded ROM""" """Gets the system name for the currently loaded ROM"""
res = (await send_requests(ctx, [{"type": "HASH"}]))[0] res = (await send_requests(ctx, [{"type": "HASH"}]))[0]
if res["type"] != "HASH_RESPONSE": if res["type"] != "HASH_RESPONSE":
@@ -160,16 +160,6 @@ async def get_hash(ctx: BizHawkContext) -> str:
return res["value"] return res["value"]
async def get_memory_size(ctx: BizHawkContext, domain: str) -> int:
"""Gets the size in bytes of the specified memory domain"""
res = (await send_requests(ctx, [{"type": "MEMORY_SIZE", "domain": domain}]))[0]
if res["type"] != "MEMORY_SIZE_RESPONSE":
raise SyncError(f"Expected response of type MEMORY_SIZE_RESPONSE but got {res['type']}")
return res["value"]
async def get_system(ctx: BizHawkContext) -> str: async def get_system(ctx: BizHawkContext) -> str:
"""Gets the system name for the currently loaded ROM""" """Gets the system name for the currently loaded ROM"""
res = (await send_requests(ctx, [{"type": "SYSTEM"}]))[0] res = (await send_requests(ctx, [{"type": "SYSTEM"}]))[0]
@@ -180,7 +170,7 @@ async def get_system(ctx: BizHawkContext) -> str:
return res["value"] return res["value"]
async def get_cores(ctx: BizHawkContext) -> dict[str, str]: async def get_cores(ctx: BizHawkContext) -> typing.Dict[str, str]:
"""Gets the preferred cores for systems with multiple cores. Only systems with multiple available cores have """Gets the preferred cores for systems with multiple cores. Only systems with multiple available cores have
entries.""" entries."""
res = (await send_requests(ctx, [{"type": "PREFERRED_CORES"}]))[0] res = (await send_requests(ctx, [{"type": "PREFERRED_CORES"}]))[0]
@@ -233,8 +223,8 @@ async def set_message_interval(ctx: BizHawkContext, value: float) -> None:
raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}") raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}")
async def guarded_read(ctx: BizHawkContext, read_list: Sequence[tuple[int, int, str]], async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]],
guard_list: Sequence[tuple[int, Sequence[int], str]]) -> list[bytes] | None: guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]:
"""Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected """Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected
value. value.
@@ -262,7 +252,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: Sequence[tuple[int, int,
"domain": domain "domain": domain
} for address, size, domain in read_list]) } for address, size, domain in read_list])
ret: list[bytes] = [] ret: typing.List[bytes] = []
for item in res: for item in res:
if item["type"] == "GUARD_RESPONSE": if item["type"] == "GUARD_RESPONSE":
if not item["value"]: if not item["value"]:
@@ -276,7 +266,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: Sequence[tuple[int, int,
return ret return ret
async def read(ctx: BizHawkContext, read_list: Sequence[tuple[int, int, str]]) -> list[bytes]: async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
"""Reads data at 1 or more addresses. """Reads data at 1 or more addresses.
Items in `read_list` should be organized `(address, size, domain)` where Items in `read_list` should be organized `(address, size, domain)` where
@@ -288,8 +278,8 @@ async def read(ctx: BizHawkContext, read_list: Sequence[tuple[int, int, str]]) -
return await guarded_read(ctx, read_list, []) return await guarded_read(ctx, read_list, [])
async def guarded_write(ctx: BizHawkContext, write_list: Sequence[tuple[int, Sequence[int], str]], async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]],
guard_list: Sequence[tuple[int, Sequence[int], str]]) -> bool: guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool:
"""Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value. """Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value.
Items in `write_list` should be organized `(address, value, domain)` where Items in `write_list` should be organized `(address, value, domain)` where
@@ -326,7 +316,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: Sequence[tuple[int, Seq
return True return True
async def write(ctx: BizHawkContext, write_list: Sequence[tuple[int, Sequence[int], str]]) -> None: async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None:
"""Writes data to 1 or more addresses. """Writes data to 1 or more addresses.
Items in write_list should be organized `(address, value, domain)` where Items in write_list should be organized `(address, value, domain)` where

View File

@@ -5,9 +5,9 @@ A module containing the BizHawkClient base class and metaclass
from __future__ import annotations from __future__ import annotations
import abc import abc
from typing import TYPE_CHECKING, Any, ClassVar from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Tuple, Union
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch as launch_component from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess
if TYPE_CHECKING: if TYPE_CHECKING:
from .context import BizHawkClientContext from .context import BizHawkClientContext
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
def launch_client(*args) -> None: def launch_client(*args) -> None:
from .context import launch from .context import launch
launch_component(launch, name="BizHawkClient", args=args) launch_subprocess(launch, name="BizHawkClient", args=args)
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
@@ -24,9 +24,9 @@ components.append(component)
class AutoBizHawkClientRegister(abc.ABCMeta): class AutoBizHawkClientRegister(abc.ABCMeta):
game_handlers: ClassVar[dict[tuple[str, ...], dict[str, BizHawkClient]]] = {} game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {}
def __new__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]) -> AutoBizHawkClientRegister: def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister:
new_class = super().__new__(cls, name, bases, namespace) new_class = super().__new__(cls, name, bases, namespace)
# Register handler # Register handler
@@ -54,7 +54,7 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
return new_class return new_class
@staticmethod @staticmethod
async def get_handler(ctx: "BizHawkClientContext", system: str) -> BizHawkClient | None: async def get_handler(ctx: "BizHawkClientContext", system: str) -> Optional[BizHawkClient]:
for systems, handlers in AutoBizHawkClientRegister.game_handlers.items(): for systems, handlers in AutoBizHawkClientRegister.game_handlers.items():
if system in systems: if system in systems:
for handler in handlers.values(): for handler in handlers.values():
@@ -65,13 +65,13 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister): class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
system: ClassVar[str | tuple[str, ...]] system: ClassVar[Union[str, Tuple[str, ...]]]
"""The system(s) that the game this client is for runs on""" """The system(s) that the game this client is for runs on"""
game: ClassVar[str] game: ClassVar[str]
"""The game this client is for""" """The game this client is for"""
patch_suffix: ClassVar[str | tuple[str, ...] | None] patch_suffix: ClassVar[Optional[Union[str, Tuple[str, ...]]]]
"""The file extension(s) this client is meant to open and patch (e.g. ".apz3")""" """The file extension(s) this client is meant to open and patch (e.g. ".apz3")"""
@abc.abstractmethod @abc.abstractmethod

View File

@@ -6,7 +6,7 @@ checking or launching the client, otherwise it will probably cause circular impo
import asyncio import asyncio
import enum import enum
import subprocess import subprocess
from typing import Any from typing import Any, Dict, Optional
from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, server_loop, logger, gui_enabled from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, server_loop, logger, gui_enabled
import Patch import Patch
@@ -43,15 +43,15 @@ class BizHawkClientContext(CommonContext):
command_processor = BizHawkClientCommandProcessor command_processor = BizHawkClientCommandProcessor
auth_status: AuthStatus auth_status: AuthStatus
password_requested: bool password_requested: bool
client_handler: BizHawkClient | None client_handler: Optional[BizHawkClient]
slot_data: dict[str, Any] | None = None slot_data: Optional[Dict[str, Any]] = None
rom_hash: str | None = None rom_hash: Optional[str] = None
bizhawk_ctx: BizHawkContext bizhawk_ctx: BizHawkContext
watcher_timeout: float watcher_timeout: float
"""The maximum amount of time the game watcher loop will wait for an update from the server before executing""" """The maximum amount of time the game watcher loop will wait for an update from the server before executing"""
def __init__(self, server_address: str | None, password: str | None): def __init__(self, server_address: Optional[str], password: Optional[str]):
super().__init__(server_address, password) super().__init__(server_address, password)
self.auth_status = AuthStatus.NOT_AUTHENTICATED self.auth_status = AuthStatus.NOT_AUTHENTICATED
self.password_requested = False self.password_requested = False
@@ -231,28 +231,20 @@ async def _run_game(rom: str):
) )
def _patch_and_run_game(patch_file: str): async def _patch_and_run_game(patch_file: str):
try: try:
metadata, output_file = Patch.create_rom_file(patch_file) metadata, output_file = Patch.create_rom_file(patch_file)
Utils.async_start(_run_game(output_file)) Utils.async_start(_run_game(output_file))
return metadata
except Exception as exc: except Exception as exc:
logger.exception(exc) logger.exception(exc)
Utils.messagebox("Error Patching Game", str(exc), True)
return {}
def launch(*launch_args: str) -> None: def launch(*launch_args) -> None:
async def main(): async def main():
parser = get_base_parser() parser = get_base_parser()
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file") parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file")
args = parser.parse_args(launch_args) args = parser.parse_args(launch_args)
if args.patch_file != "":
metadata = _patch_and_run_game(args.patch_file)
if "server" in metadata:
args.connect = metadata["server"]
ctx = BizHawkClientContext(args.connect, args.password) ctx = BizHawkClientContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
@@ -260,6 +252,9 @@ def launch(*launch_args: str) -> None:
ctx.run_gui() ctx.run_gui()
ctx.run_cli() ctx.run_cli()
if args.patch_file != "":
Utils.async_start(_patch_and_run_game(args.patch_file))
watcher_task = asyncio.create_task(_game_watcher(ctx), name="GameWatcher") watcher_task = asyncio.create_task(_game_watcher(ctx), name="GameWatcher")
try: try:

View File

@@ -1,8 +1,9 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from typing import Dict
from Options import Choice, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions from dataclasses import dataclass
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
class FreeincarnateMax(Range): class FreeincarnateMax(Range):

View File

@@ -1,6 +1,6 @@
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
from Options import PerGameCommonOptions from Options import PerGameCommonOptions
from .Locations import location_table, AdventureLocation, dragon_room_to_region from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,

View File

@@ -2,14 +2,14 @@ import hashlib
import json import json
import os import os
import zipfile import zipfile
from typing import Any from typing import Optional, Any
import bsdiff4
import Utils import Utils
from .Locations import AdventureLocation, LocationData
from settings import get_settings from settings import get_settings
from worlds.Files import APPatch, AutoPatchRegister from worlds.Files import APPatch, AutoPatchRegister
from .Locations import LocationData
import bsdiff4
ADVENTUREHASH: str = "157bddb7192754a45372be196797f284" ADVENTUREHASH: str = "157bddb7192754a45372be196797f284"

View File

@@ -1,24 +1,35 @@
import base64
import copy import copy
import itertools
import math import math
import os import os
import typing
from typing import ClassVar, Dict, Optional, Tuple
import settings import settings
from BaseClasses import Item, ItemClassification, MultiWorld, Tutorial, LocationProgressType import typing
from enum import IntFlag
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial, \
LocationProgressType
from Utils import __version__ from Utils import __version__
from Options import AssembleOptions
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from worlds.LauncherComponents import Component, components, SuffixIdentifier from Fill import fill_restrictive
from worlds.generic.Rules import add_rule, set_rule
from .Options import DragonRandoType, DifficultySwitchA, DifficultySwitchB, \
AdventureOptions
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
AdventureAutoCollectLocation
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
from .Locations import location_table, base_location_id, LocationData, get_random_room_in_regions from .Locations import location_table, base_location_id, LocationData, get_random_room_in_regions
from .Offsets import static_item_data_location, items_ram_start, static_item_element_size, item_position_table, \ from .Offsets import static_item_data_location, items_ram_start, static_item_element_size, item_position_table, \
static_first_dragon_index, connector_port_offset, yorgle_speed_data_location, grundle_speed_data_location, \ static_first_dragon_index, connector_port_offset, yorgle_speed_data_location, grundle_speed_data_location, \
rhindle_speed_data_location, item_ram_addresses, start_castle_values, start_castle_offset rhindle_speed_data_location, item_ram_addresses, start_castle_values, start_castle_offset
from .Options import DragonRandoType, DifficultySwitchA, DifficultySwitchB, AdventureOptions
from .Regions import create_regions from .Regions import create_regions
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, AdventureAutoCollectLocation
from .Rules import set_rules from .Rules import set_rules
from worlds.LauncherComponents import Component, components, SuffixIdentifier
# Adventure # Adventure
components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn'))) components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn')))

View File

@@ -141,12 +141,9 @@ def set_dw_rules(world: "HatInTimeWorld"):
add_dw_rules(world, all_clear) add_dw_rules(world, all_clear)
add_rule(main_stamp, main_objective.access_rule) add_rule(main_stamp, main_objective.access_rule)
add_rule(all_clear, main_objective.access_rule) add_rule(all_clear, main_objective.access_rule)
# Only set bonus stamp rules to require All Clear if we don't auto complete bonuses # Only set bonus stamp rules if we don't auto complete bonuses
if not world.options.DWAutoCompleteBonuses and not world.is_bonus_excluded(all_clear.name): if not world.options.DWAutoCompleteBonuses and not world.is_bonus_excluded(all_clear.name):
add_rule(bonus_stamps, all_clear.access_rule) add_rule(bonus_stamps, all_clear.access_rule)
else:
# As soon as the Main Objective is completed, the bonuses auto-complete.
add_rule(bonus_stamps, main_objective.access_rule)
if world.options.DWShuffle: if world.options.DWShuffle:
for i in range(len(world.dw_shuffle)-1): for i in range(len(world.dw_shuffle)-1):
@@ -346,7 +343,6 @@ def create_enemy_events(world: "HatInTimeWorld"):
def set_enemy_rules(world: "HatInTimeWorld"): def set_enemy_rules(world: "HatInTimeWorld"):
no_tourist = "Camera Tourist" in world.excluded_dws or "Camera Tourist" in world.excluded_bonuses no_tourist = "Camera Tourist" in world.excluded_dws or "Camera Tourist" in world.excluded_bonuses
difficulty = get_difficulty(world)
for enemy, regions in hit_list.items(): for enemy, regions in hit_list.items():
if no_tourist and enemy in bosses: if no_tourist and enemy in bosses:
@@ -376,14 +372,6 @@ def set_enemy_rules(world: "HatInTimeWorld"):
or state.has("Zipline Unlock - The Lava Cake Path", world.player) or state.has("Zipline Unlock - The Lava Cake Path", world.player)
or state.has("Zipline Unlock - The Windmill Path", world.player)) or state.has("Zipline Unlock - The Windmill Path", world.player))
elif enemy == "Toilet":
if area == "Toilet of Doom":
# The boss firewall is in the way and can only be skipped on Expert logic using a cherry hover.
add_rule(event, lambda state: has_paintings(state, world, 1, allow_skip=difficulty == Difficulty.EXPERT))
if difficulty < Difficulty.HARD:
# Hard logic and above can cross the boss arena gap with a cherry bridge.
add_rule(event, lambda state: can_use_hookshot(state, world))
elif enemy == "Director": elif enemy == "Director":
if area == "Dead Bird Studio Basement": if area == "Dead Bird Studio Basement":
add_rule(event, lambda state: can_use_hookshot(state, world)) add_rule(event, lambda state: can_use_hookshot(state, world))
@@ -442,7 +430,7 @@ hit_list = {
# Bosses # Bosses
"Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"], "Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"],
"Director": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"], "Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"],
"Toilet": ["Toilet of Doom", "Boss Rush"], "Toilet": ["Toilet of Doom", "Boss Rush"],
"Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush", "Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush",
@@ -466,7 +454,7 @@ triple_enemy_locations = [
bosses = [ bosses = [
"Mafia Boss", "Mafia Boss",
"Director", "Conductor",
"Toilet", "Toilet",
"Snatcher", "Snatcher",
"Toxic Flower", "Toxic Flower",

View File

@@ -264,6 +264,7 @@ ahit_locations = {
required_hats=[HatType.DWELLER], paintings=3), required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Tall Tree Hookshot Swing": LocData(2000324766, "Subcon Forest Area", "Subcon Forest - Tall Tree Hookshot Swing": LocData(2000324766, "Subcon Forest Area",
required_hats=[HatType.DWELLER],
hookshot=True, hookshot=True,
paintings=3), paintings=3),
@@ -322,7 +323,7 @@ ahit_locations = {
"Alpine Skyline - The Twilight Path": LocData(2000334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]), "Alpine Skyline - The Twilight Path": LocData(2000334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]),
"Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"), "Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"),
"Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"), "Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"),
"Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area (TIHS)", hookshot=True), "Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area"),
"Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)", hookshot=True), "Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)", hookshot=True),
"Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"), "Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"),
"Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"), "Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"),
@@ -406,7 +407,7 @@ act_completions = {
hit_type=HitType.umbrella_or_brewing, hookshot=True, paintings=1), hit_type=HitType.umbrella_or_brewing, hookshot=True, paintings=1),
"Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor", "Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor",
hit_type=HitType.dweller_bell, paintings=1), hit_type=HitType.umbrella, paintings=1),
"Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service", "Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service",
required_hats=[HatType.SPRINT]), required_hats=[HatType.SPRINT]),
@@ -877,7 +878,7 @@ snatcher_coins = {
dlc_flags=HatDLC.death_wish), dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of HQ (DW: BTH)": LocData(0, "Beat the Heat", snatcher_coin="Snatcher Coin - Top of HQ", "Snatcher Coin - Top of HQ (DW: BTH)": LocData(0, "Beat the Heat", snatcher_coin="Snatcher Coin - Top of HQ",
hit_type=HitType.umbrella, dlc_flags=HatDLC.death_wish), dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", snatcher_coin="Snatcher Coin - Top of Tower", "Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", snatcher_coin="Snatcher Coin - Top of Tower",
dlc_flags=HatDLC.death_wish), dlc_flags=HatDLC.death_wish),

View File

@@ -338,7 +338,7 @@ class MinExtraYarn(Range):
There must be at least this much more yarn over the total number of yarn needed to craft all hats. There must be at least this much more yarn over the total number of yarn needed to craft all hats.
For example, if this option's value is 10, and the total yarn needed to craft all hats is 40, For example, if this option's value is 10, and the total yarn needed to craft all hats is 40,
there must be at least 50 yarn in the pool.""" there must be at least 50 yarn in the pool."""
display_name = "Min Extra Yarn" display_name = "Max Extra Yarn"
range_start = 5 range_start = 5
range_end = 15 range_end = 15
default = 10 default = 10

View File

@@ -414,7 +414,7 @@ def set_moderate_rules(world: "HatInTimeWorld"):
# Moderate: Mystifying Time Mesa time trial without hats # Moderate: Mystifying Time Mesa time trial without hats
set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player),
lambda state: True) lambda state: can_use_hookshot(state, world))
# Moderate: Goat Refinery from TIHS with Sprint only # Moderate: Goat Refinery from TIHS with Sprint only
add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player), add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player),
@@ -493,6 +493,9 @@ def set_hard_rules(world: "HatInTimeWorld"):
lambda state: has_paintings(state, world, 3, True)) lambda state: has_paintings(state, world, 3, True))
# SDJ # SDJ
add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player),
lambda state: can_use_hat(state, world, HatType.SPRINT) and has_paintings(state, world, 2), "or")
add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player),
lambda state: can_use_hat(state, world, HatType.SPRINT), "or") lambda state: can_use_hat(state, world, HatType.SPRINT), "or")
@@ -530,10 +533,7 @@ def set_expert_rules(world: "HatInTimeWorld"):
# Expert: Mafia Town - Above Boats, Top of Lighthouse, and Hot Air Balloon with nothing # Expert: Mafia Town - Above Boats, Top of Lighthouse, and Hot Air Balloon with nothing
set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True) set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Mafia Town - Top of Lighthouse", world.player), lambda state: True) set_rule(world.multiworld.get_location("Mafia Town - Top of Lighthouse", world.player), lambda state: True)
# There are not enough buckets/beach balls to bucket/ball hover in Heating Up Mafia Town, so any other Mafia Town set_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player), lambda state: True)
# act is required.
add_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player),
lambda state: state.can_reach_region("Mafia Town Area", world.player), "or")
# Expert: Clear Dead Bird Studio with nothing # Expert: Clear Dead Bird Studio with nothing
for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations: for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations:
@@ -590,7 +590,7 @@ def set_expert_rules(world: "HatInTimeWorld"):
if world.is_dlc2(): if world.is_dlc2():
# Expert: clear Rush Hour with nothing # Expert: clear Rush Hour with nothing
if world.options.NoTicketSkips != NoTicketSkips.option_true: if not world.options.NoTicketSkips:
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True)
else: else:
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
@@ -739,7 +739,7 @@ def set_dlc1_rules(world: "HatInTimeWorld"):
# This particular item isn't present in Act 3 for some reason, yes in vanilla too # This particular item isn't present in Act 3 for some reason, yes in vanilla too
add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player), add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player),
lambda state: (state.can_reach("Bon Voyage!", "Region", world.player) and can_use_hookshot(state, world)) lambda state: state.can_reach("Bon Voyage!", "Region", world.player)
or state.can_reach("Ship Shape", "Region", world.player)) or state.can_reach("Ship Shape", "Region", world.player))

View File

@@ -12,13 +12,13 @@ from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
from worlds.AutoWorld import World, WebWorld, CollectionState from worlds.AutoWorld import World, WebWorld, CollectionState
from worlds.generic.Rules import add_rule from worlds.generic.Rules import add_rule
from typing import List, Dict, TextIO from typing import List, Dict, TextIO
from worlds.LauncherComponents import Component, components, icon_paths, launch as launch_component, Type from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
from Utils import local_path from Utils import local_path
def launch_client(): def launch_client():
from .Client import launch from .Client import launch
launch_component(launch, name="AHITClient") launch_subprocess(launch, name="AHITClient")
components.append(Component("A Hat in Time Client", "AHITClient", func=launch_client, components.append(Component("A Hat in Time Client", "AHITClient", func=launch_client,

View File

@@ -21,7 +21,7 @@
3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`. 3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`.
While it downloads, you can subscribe to the [Archipelago workshop mod](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601). While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601))
4. Once the game finishes downloading, start it up. 4. Once the game finishes downloading, start it up.
@@ -62,4 +62,4 @@ The level that the relic set unlocked will stay unlocked.
### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work! ### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
if you have too many save files. Delete them and it should fix the problem. if you have too many save files. Delete them and it should fix the problem.

View File

@@ -119,9 +119,7 @@ def KholdstareDefeatRule(state, player: int) -> bool:
def VitreousDefeatRule(state, player: int) -> bool: def VitreousDefeatRule(state, player: int) -> bool:
return ((can_shoot_arrows(state, player) and can_use_bombs(state, player, 10)) return can_shoot_arrows(state, player) or has_melee_weapon(state, player)
or can_shoot_arrows(state, player, 35) or state.has("Silver Bow", player)
or has_melee_weapon(state, player))
def TrinexxDefeatRule(state, player: int) -> bool: def TrinexxDefeatRule(state, player: int) -> bool:

View File

@@ -464,7 +464,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
snes_logger.info(f"Discarding recent {len(new_locations)} checks as ROM Status has changed.") snes_logger.info(f"Discarding recent {len(new_locations)} checks as ROM Status has changed.")
return False return False
else: else:
await ctx.check_locations(new_locations) await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
await snes_flush_writes(ctx) await snes_flush_writes(ctx)
return True return True

View File

@@ -484,7 +484,8 @@ def generate_itempool(world):
if multiworld.randomize_cost_types[player]: if multiworld.randomize_cost_types[player]:
# Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic # Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic
for item in items: for item in items:
if item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart"): if (item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart")
or "Arrow Upgrade" in item.name):
item.classification = ItemClassification.progression item.classification = ItemClassification.progression
else: else:
# Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
@@ -712,7 +713,7 @@ def get_pool_core(world, player: int):
pool.remove("Rupees (20)") pool.remove("Rupees (20)")
if retro_bow: if retro_bow:
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (70)'} replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (50)'}
pool = ['Rupees (5)' if item in replace else item for item in pool] pool = ['Rupees (5)' if item in replace else item for item in pool]
if world.small_key_shuffle[player] == small_key_shuffle.option_universal: if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
pool.extend(diff.universal_keys) pool.extend(diff.universal_keys)

View File

@@ -7,7 +7,7 @@ from worlds.AutoWorld import World
def GetBeemizerItem(world, player: int, item): def GetBeemizerItem(world, player: int, item):
item_name = item if isinstance(item, str) else item.name item_name = item if isinstance(item, str) else item.name
if item_name not in trap_replaceable or player in world.groups: if item_name not in trap_replaceable:
return item return item
# first roll - replaceable item should be replaced, within beemizer_total_chance # first roll - replaceable item should be replaced, within beemizer_total_chance
@@ -110,9 +110,9 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\
'Crystal 7': ItemData(IC.progression, 'Crystal', (0x08, 0x34, 0x64, 0x40, 0x7C, 0x06), None, None, None, None, None, None, "a blue crystal"), 'Crystal 7': ItemData(IC.progression, 'Crystal', (0x08, 0x34, 0x64, 0x40, 0x7C, 0x06), None, None, None, None, None, None, "a blue crystal"),
'Single Arrow': ItemData(IC.filler, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), 'Single Arrow': ItemData(IC.filler, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'),
'Arrows (10)': ItemData(IC.filler, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack','stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again','ten arrows'), 'Arrows (10)': ItemData(IC.filler, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack','stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again','ten arrows'),
'Arrow Upgrade (+10)': ItemData(IC.progression_skip_balancing, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), 'Arrow Upgrade (+10)': ItemData(IC.useful, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Arrow Upgrade (+5)': ItemData(IC.progression_skip_balancing, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), 'Arrow Upgrade (+5)': ItemData(IC.useful, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Arrow Upgrade (70)': ItemData(IC.progression_skip_balancing, None, 0x4D, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), 'Arrow Upgrade (70)': ItemData(IC.useful, None, 0x4D, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Single Bomb': ItemData(IC.filler, None, 0x27, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'), 'Single Bomb': ItemData(IC.filler, None, 0x27, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'),
'Bombs (3)': ItemData(IC.filler, None, 0x28, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'), 'Bombs (3)': ItemData(IC.filler, None, 0x28, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'),
'Bombs (10)': ItemData(IC.filler, None, 0x31, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'), 'Bombs (10)': ItemData(IC.filler, None, 0x31, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'),

View File

@@ -1125,7 +1125,7 @@ def set_trock_key_rules(world, player):
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']: for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']:
set_rule(world.get_entrance(entrance, player), lambda state: False) set_rule(world.get_entrance(entrance, player), lambda state: False)
all_state = world.get_all_state(use_cache=False, allow_partial_entrances=True) all_state = world.get_all_state(use_cache=False)
all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work
all_state.stale[player] = True all_state.stale[player] = True

View File

@@ -170,8 +170,7 @@ def push_shop_inventories(multiworld):
# Retro Bow arrows will already have been pushed # Retro Bow arrows will already have been pushed
if (not multiworld.retro_bow[location.player]) or ((item_name, location.item.player) if (not multiworld.retro_bow[location.player]) or ((item_name, location.item.player)
!= ("Single Arrow", location.player)): != ("Single Arrow", location.player)):
location.shop.push_inventory(location.shop_slot, item_name, location.shop.push_inventory(location.shop_slot, item_name, location.shop_price,
round(location.shop_price * get_price_modifier(location.item)),
1, location.item.player if location.item.player != location.player else 0, 1, location.item.player if location.item.player != location.player else 0,
location.shop_price_type) location.shop_price_type)
location.shop_price = location.shop.inventory[location.shop_slot]["price"] = min(location.shop_price, location.shop_price = location.shop.inventory[location.shop_slot]["price"] = min(location.shop_price,

View File

@@ -15,18 +15,18 @@ def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bo
def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool: def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for
shop in state.multiworld.shops) shop in state.multiworld.shops)
def can_buy(state: CollectionState, item: str, player: int) -> bool: def can_buy(state: CollectionState, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for
shop in state.multiworld.shops) shop in state.multiworld.shops)
def can_shoot_arrows(state: CollectionState, player: int, count: int = 0) -> bool: def can_shoot_arrows(state: CollectionState, player: int) -> bool:
if state.multiworld.retro_bow[player]: if state.multiworld.retro_bow[player]:
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player) return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player)
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_hold_arrows(state, player, count) return state.has('Bow', player) or state.has('Silver Bow', player)
def has_triforce_pieces(state: CollectionState, player: int) -> bool: def has_triforce_pieces(state: CollectionState, player: int) -> bool:
@@ -61,13 +61,13 @@ def heart_count(state: CollectionState, player: int) -> int:
# Warning: This only considers items that are marked as advancement items # Warning: This only considers items that are marked as advancement items
diff = state.multiworld.worlds[player].difficulty_requirements diff = state.multiworld.worlds[player].difficulty_requirements
return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \ return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \
+ state.count('Sanctuary Heart Container', player) \ + state.count('Sanctuary Heart Container', player) \
+ min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ + min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
+ 3 # starting hearts + 3 # starting hearts
def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16, def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has. fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
basemagic = 8 basemagic = 8
if state.has('Magic Upgrade (1/4)', player): if state.has('Magic Upgrade (1/4)', player):
basemagic = 32 basemagic = 32
@@ -84,18 +84,11 @@ def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
def can_hold_arrows(state: CollectionState, player: int, quantity: int): def can_hold_arrows(state: CollectionState, player: int, quantity: int):
if state.multiworld.worlds[player].options.shuffle_capacity_upgrades: arrows = 30 + ((state.count("Arrow Upgrade (+5)", player) * 5) + (state.count("Arrow Upgrade (+10)", player) * 10)
if quantity == 0: + (state.count("Bomb Upgrade (50)", player) * 50))
return True # Arrow Upgrade (+5) beyond the 6th gives +10
if state.has("Arrow Upgrade (70)", player): arrows += max(0, ((state.count("Arrow Upgrade (+5)", player) - 6) * 10))
arrows = 70 return min(70, arrows) >= quantity
else:
arrows = (30 + (state.count("Arrow Upgrade (+5)", player) * 5)
+ (state.count("Arrow Upgrade (+10)", player) * 10))
# Arrow Upgrade (+5) beyond the 6th gives +10
arrows += max(0, ((state.count("Arrow Upgrade (+5)", player) - 6) * 10))
return min(70, arrows) >= quantity
return quantity <= 30 or state.has("Capacity Upgrade Shop", player)
def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool: def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool:
@@ -153,19 +146,19 @@ def can_get_good_bee(state: CollectionState, player: int) -> bool:
def can_retrieve_tablet(state: CollectionState, player: int) -> bool: def can_retrieve_tablet(state: CollectionState, player: int) -> bool:
return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or
(state.multiworld.swordless[player] and (state.multiworld.swordless[player] and
state.has("Hammer", player))) state.has("Hammer", player)))
def has_sword(state: CollectionState, player: int) -> bool: def has_sword(state: CollectionState, player: int) -> bool:
return state.has('Fighter Sword', player) \ return state.has('Fighter Sword', player) \
or state.has('Master Sword', player) \ or state.has('Master Sword', player) \
or state.has('Tempered Sword', player) \ or state.has('Tempered Sword', player) \
or state.has('Golden Sword', player) or state.has('Golden Sword', player)
def has_beam_sword(state: CollectionState, player: int) -> bool: def has_beam_sword(state: CollectionState, player: int) -> bool:
return state.has('Master Sword', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', return state.has('Master Sword', player) or state.has('Tempered Sword', player) or state.has('Golden Sword',
player) player)
def has_melee_weapon(state: CollectionState, player: int) -> bool: def has_melee_weapon(state: CollectionState, player: int) -> bool:
@@ -178,9 +171,9 @@ def has_fire_source(state: CollectionState, player: int) -> bool:
def can_melt_things(state: CollectionState, player: int) -> bool: def can_melt_things(state: CollectionState, player: int) -> bool:
return state.has('Fire Rod', player) or \ return state.has('Fire Rod', player) or \
(state.has('Bombos', player) and (state.has('Bombos', player) and
(state.multiworld.swordless[player] or (state.multiworld.swordless[player] or
has_sword(state, player))) has_sword(state, player)))
def has_misery_mire_medallion(state: CollectionState, player: int) -> bool: def has_misery_mire_medallion(state: CollectionState, player: int) -> bool:

View File

@@ -1,123 +1,224 @@
# Guía de instalación para A Link to the Past Randomizer Multiworld # Guía de instalación para A Link to the Past Randomizer Multiworld
<div id="tutorial-video-container">
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/mJKEHaiyR_Y" frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
</iframe>
</div>
## Software requerido ## Software requerido
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [SNI](https://github.com/alttpo/sni/releases). Esto está incluido automáticamente en la instalación de Archipelago. - [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities)
- SNI no es compatible con (Q)Usb2Snes. - Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES, por ejemplo: - Un emulador capaz de ejecutar scripts Lua
- Un emulador capaz de conectarse a SNI ([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
([snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases), [snes9x-rr](https://github.com/gocha/snes9x-rr/releases),
[BSNES-plus](https://github.com/black-sliver/bsnes-plus),
[BizHawk](https://tasvideos.org/BizHawk), o [BizHawk](https://tasvideos.org/BizHawk), o
[RetroArch](https://retroarch.com?page=platforms) 1.10.1 o más nuevo). [RetroArch](https://retroarch.com?page=platforms) 1.10.1 o más nuevo). O,
- Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), u otro hardware compatible. **nota: - Un flashcart SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), o otro hardware compatible
Las SNES minis modificadas no tienen soporte de SNI. Algunos usuarios dicen haber tenido éxito con Qusb2Snes para esta consola,
pero no tiene soporte.**
- Tu archivo ROM japones v1.0, probablemente se llame `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc` - Tu archivo ROM japones v1.0, probablemente se llame `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
## Procedimiento de instalación ## Procedimiento de instalación
1. Descarga e instala [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest). ### Instalación en Windows
**El archivo del instalador se encuentra en la sección de assets al final de la información de version**.
2. La primera vez que realices una generación local o parchees tu juego, se te pedirá que ubiques tu archivo ROM base. 1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente.
Este es tu archivo ROM de Link to the Past japonés. Esto sólo debe hacerse una vez. **El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu
intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.Archipelago.exe`
4. Si estás usando un emulador, deberías de asignar tu emulador con compatibilidad con Lua como el programa por defecto para abrir archivos - Si estas interesado en jugar la variante que aleatoriza las puertas internas de las mazmorras, necesitaras bajar '
ROM. Setup.BerserkerMultiWorld.Doors.exe'
1. Extrae la carpeta de tu emulador al Escritorio, o algún otro sitio que vayas a recordar. - Durante el proceso de instalación, se te pedirá donde esta situado tu archivo ROM japonés v1.0. Si ya habías
2. Haz click derecho en un archivo ROM y selecciona **Abrir con...** instalado este software con anterioridad y simplemente estas actualizando, no se te pedirá la localización del
3. Marca la casilla junto a **Usar siempre este programa para abrir archivos .sfc** archivo una segunda vez.
4. Baja al final de la lista y haz click en el texto gris **Buscar otro programa en este PC** - Puede ser que el programa pida la instalación de Microsoft Visual C++. Si ya lo tienes en tu ordenador (
5. Busca el archivo `.exe` de tu emulador y haz click en **Abrir**. Este archivo debería de encontrarse dentro de la carpeta que posiblemente por que un juego de Steam ya lo haya instalado), el instalador no te pedirá su instalación.
extrajiste en el paso uno.
2. Si estas usando un emulador, deberías asignar la versión capaz de ejecutar scripts Lua como programa por defecto para
lanzar ficheros de ROM de SNES.
1. Extrae tu emulador al escritorio, o cualquier sitio que después recuerdes.
2. Haz click derecho en un fichero de ROM (ha de tener la extensión sfc) y selecciona **Abrir con...**
3. Marca la opción **Usar siempre esta aplicación para abrir los archivos .sfc**
4. Baja hasta el final de la lista y haz click en la opción **Buscar otra aplicación en el equipo** (Si usas Windows
10 es posible que debas hacer click en **Más aplicaciones**)
5. Busca el archivo .exe de tu emulador y haz click en **Abrir**. Este archivo debe estar en el directorio donde
extrajiste en el paso 1.
### Instalación en Macintosh
- ¡Necesitamos voluntarios para rellenar esta seccion! Contactad con **Farrak Kilhn** (en inglés) en Discord si queréis
ayudar.
## Configurar tu archivo YAML
### Que es un archivo YAML y por qué necesito uno?
Tu archivo YAML contiene un conjunto de opciones de configuración que proveen al generador con información sobre como
debe generar tu juego. Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta configuración
permite que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida
de multiworld puede tener diferentes opciones.
### Donde puedo obtener un fichero YAML?
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-options)" en el sitio web te permite configurar tu
configuración personal y descargar un fichero "YAML".
### Configuración YAML avanzada
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina
["Weighted settings"](/games/A Link to the Past/weighted-options),
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones
representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categoría pueden ser
elegidos sobre otros de la misma.
Por ejemplo, imagina que el generador crea un cubo llamado "map_shuffle", y pone trozos de papel doblado en él por cada
sub-opción. Ademas imaginemos que tu valor elegido para "on" es 20 y el elegido para "off" es 40.
Por tanto, en este ejemplo, habrán 60 trozos de papel. 20 para "on" y 40 para "off". Cuando el generador esta decidiendo
si activar o no "map shuffle" para tu partida, meterá la mano en el cubo y sacara un trozo de papel al azar. En este
ejemplo, es mucho mas probable (2 de cada 3 veces (40/60)) que "map shuffle" esté desactivado.
Si quieres que una opción no pueda ser escogida, simplemente asigna el valor 0 a dicha opción. Recuerda que cada opción
debe tener al menos un valor mayor que cero, si no la generación fallará.
### Verificando tu archivo YAML
Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina
[YAML Validator](/check).
## Generar una partida para un jugador
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-options), configura tus opciones, haz
click en el boton "Generate game".
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el Cliente no
es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld
WebUI") que se ha abierto automáticamente.
## Unirse a una partida MultiWorld
### Obtener el fichero de parche y crea tu ROM ### Obtener el fichero de parche y crea tu ROM
Cuando te unas a una partida multiworld, se te pedirá enviarle tu archivo de configuración a quien quiera que esté creando. Una vez eso Cuando te unes a una partida multiworld, debes proveer tu fichero YAML a quien sea el creador de la partida. Una vez
este hecho, el creador te devolverá un enlace para descargar el parche o un fichero zip conteniendo todos los ficheros este hecho, el creador te devolverá un enlace para descargar el parche o un fichero zip conteniendo todos los ficheros
de parche de la partida. Tu fichero de parche debe de tener la extensión `.aplttp`. de parche de la partida Tu fichero de parche debe tener la extensión `.aplttp`.
Pon tu fichero de parche en el escritorio o en algún sitio conveniente, y hazle doble click. Esto debería ejecutar Pon tu fichero de parche en el escritorio o en algún sitio conveniente, y haz doble click. Esto debería ejecutar
automáticamente el cliente, y además creará la rom en el mismo directorio donde este el fichero de parche. automáticamente el cliente, y ademas creara la rom en el mismo directorio donde este el fichero de parche.
### Conectar al cliente ### Conectar al cliente
#### Con emulador #### Con emulador
Cuando el cliente se lance automáticamente, SNI debería de ejecutarse en segundo plano. Si es la Cuando el cliente se lance automáticamente, QUsb2Snes debería haberse ejecutado también. Si es la primera vez que lo
primera vez que se ejecuta, tal vez se te pida permitir que se comunique a través del firewall de Windows ejecutas, puedes ser que el firewall de Windows te pregunte si le permites la comunicación.
#### snes9x-nwa
1. Haz click en el menu Network y marca 'Enable Emu Network Control
2. Carga tu archivo ROM si no lo habías hecho antes
##### snes9x-rr ##### snes9x-rr
1. Carga tu fichero ROM, si no lo has hecho ya 1. Carga tu fichero de ROM, si no lo has hecho ya
2. Abre el menu "File" y situa el raton en **Lua Scripting** 2. Abre el menu "File" y situa el raton en **Lua Scripting**
3. Haz click en **New Lua Script Window...** 3. Haz click en **New Lua Script Window...**
4. En la nueva ventana, haz click en **Browse...** 4. En la nueva ventana, haz click en **Browse...**
5. Selecciona el archivo lua conector incluido con tu cliente 5. Navega hacia el directorio donde este situado snes9x-rr, entra en el directorio `lua`, y
- Busca en la carpeta de Archipelago `/SNI/lua/`. escoge `multibridge.lua`
6. Si ves un error mientras carga el script que dice `socket.dll missing` o algo similar, ve a la carpeta de 6. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo
el lua que estas usando en tu gestor de archivos y copia el `socket.dll` a la raíz de tu instalación de snes9x. nombre en la esquina superior izquierda.
##### BNES-Plus
1. Cargue su archivo ROM si aún no se ha cargado.
2. El emulador debería conectarse automáticamente mientras SNI se está ejecutando.
##### BizHawk ##### BizHawk
1. Asegurate que se ha cargado el núcleo BSNES. Se hace en la barra de menú principal, bajo: 1. Asegurate que se ha cargado el nucleo BSNES. Debes hacer esto en el menu Tools y siguiento estas opciones:
- (≤ 2.8) `Config``Cores``SNES``BSNES` `Config --> Cores --> SNES --> BSNES`
- (≥ 2.9) `Config``Preferred Cores``SNES``BSNESv115+` Una vez cambiado el nucleo cargado, BizHawk ha de ser reiniciado.
2. Carga tu fichero de ROM, si no lo has hecho ya. 2. Carga tu fichero de ROM, si no lo has hecho ya.
Si has cambiado tu preferencia de núcleo tras haber cargado la ROM, no te olvides de volverlo a cargar (atajo por defecto: Ctrl+R). 3. Haz click en el menu Tools y en la opción **Lua Console**
3. Arrastra el archivo `Connector.lua` que has descargado a la ventana principal de EmuHawk. 4. Haz click en el botón para abrir un nuevo script Lua.
- Busca en la carpeta de Archipelago `/SNI/lua/`. 5. Navega al directorio de instalación de MultiWorld Utilities, y en los siguiente directorios:
- También podrías abrir la consola de Lua manualmente, hacer click en `Script``Open Script`, e ir a `Connector.lua` `QUsb2Snes/Qusb2Snes/LuaBridge`
con el selector de archivos. 6. Selecciona `luabridge.lua` y haz click en Abrir.
7. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo
nombre en la esquina superior izquierda.
##### RetroArch 1.10.1 o más nuevo ##### RetroArch 1.10.1 o más nuevo
Sólo hay que seguir estos pasos una vez. Sólo hay que segiur estos pasos una vez.
1. Comienza en la pantalla del menú principal de RetroArch. 1. Comienza en la pantalla del menú principal de RetroArch.
2. Ve a Ajustes --> Interfaz de usario. Configura "Mostrar ajustes avanzados" en ON. 2. Ve a Ajustes --> Interfaz de usario. Configura "Mostrar ajustes avanzados" en ON.
3. Ve a Ajustes --> Red. Pon "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 el valor por defecto, 3. Ve a Ajustes --> Red. Configura "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 (el
el Puerto de comandos de red. default) el Puerto de comandos de red.
![Captura de pantalla del ajuste Comandos de red](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) ![Captura de pantalla del ajuste Comandos de red](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png)
4. Ve a Menú principal --> Actualizador en línea --> Descargador de núcleos. Desplázate y selecciona "Nintendo - SNES / 4. Ve a Menú principal --> Actualizador en línea --> Descargador de núcleos. Desplázate y selecciona "Nintendo - SNES /
SFC (bsnes-mercury Performance)". SFC (bsnes-mercury Performance)".
Cuando cargas un ROM, asegúrate de seleccionar un núcleo **bsnes-mercury**. Estos son los únicos núcleos que permiten Cuando cargas un ROM, asegúrate de seleccionar un núcleo **bsnes-mercury**. Estos son los sólos núcleos que permiten
que herramientas externas lean datos del ROM. que herramientas externas lean datos del ROM.
#### Con Hardware #### Con Hardware
Esta guía asume que ya has descargado el firmware correcto para tu dispositivo. Si no lo has hecho ya, por favor hazlo ahora. Los Esta guía asume que ya has descargado el firmware correcto para tu dispositivo. Si no lo has hecho ya, hazlo ahora. Los
usuarios de SD2SNES y FXPak Pro pueden descargar el firmware apropiado usuarios de SD2SNES y FXPak Pro pueden descargar el firmware apropiado
[aqui](https://github.com/RedGuyyyy/sd2snes/releases). Puede que los usuarios de otros dispositivos encuentren informacion útil [aqui](https://github.com/RedGuyyyy/sd2snes/releases). Los usuarios de otros dispositivos pueden encontrar información
[en esta página](http://usb2snes.com/#supported-platforms). [en esta página](http://usb2snes.com/#supported-platforms).
1. Cierra tu emulador, el cual debe haberse autoejecutado. 1. Cierra tu emulador, el cual debe haberse autoejecutado.
2. Enciende tu dispositivo y carga la ROM. 2. Cierra QUsb2Snes, el cual fue ejecutado junto al cliente.
3. Ejecuta la version correcta de QUsb2Snes (v0.7.16).
4. Enciende tu dispositivo y carga la ROM.
5. Observa en el cliente que ahora muestra "SNES Device: Connected", y aparece el nombre del dispositivo.
### Conecta al Servidor Archipelago ### Conecta al MultiServer
El fichero de parche que ha lanzado el cliente debería de haberte conectado automaticamente al MultiServer. Sin embargo hay algunas El fichero de parche que ha lanzado el cliente debe haberte conectado automaticamente al MultiServer. Hay algunas
razones por las que puede que esto no suceda, como que la partida este hospedada en la página web pero generada en otra parte. Si la razonas por las que esto puede que no pase, incluyendo que el juego este hospedado en el sitio web pero se genero en
ventana del cliente muestra "Server Status: Not Connected", simplemente preguntale al creador de la partida la dirección algún otro sitio. Si el cliente muestra "Server Status: Not Connected", preguntale al creador de la partida la dirección
del servidor, cópiala en el campo "Server" y presiona Enter. del servidor, copiala en el campo "Server" y presiona Enter.
El cliente intentará conectarse a esta nueva dirección, y debería mostrar "Server Status: Connected" momentáneamente. El cliente intentara conectarse a esta nueva dirección, y debería mostrar "Server Status: Connected" en algún momento.
Si el cliente no se conecta al cabo de un rato, puede ser que necesites refrescar la pagina web.
### Jugar al juego ### Jugando
Cuando el cliente muestre tanto el dispositivo SNES como el servidor como conectados, estas listo para empezar a jugar. Felicidades por Cuando ambos SNES Device and Server aparezcan como "connected", estas listo para empezar a jugar. Felicidades por unirte
haberte unido a una partida multiworld con exito! Puedes ejecutar varios comandos en tu cliente. Para mas informacion satisfactoriamente a una partida de multiworld!
acerca de estos comando puedes usar `/help` para comandos locales del cliente y `!help` para comandos de servidor.
## Hospedando una partida de multiworld
La manera recomendad para hospedar una partida es usar el servicio proveído en
[el sitio web](/generate). El proceso es relativamente sencillo:
1. Recolecta los ficheros YAML de todos los jugadores que participen.
2. Crea un fichero ZIP conteniendo esos ficheros.
3. Carga el fichero zip en el sitio web enlazado anteriormente.
4. Espera a que la seed sea generada.
5. Cuando esto acabe, se te redigirá a una pagina titulada "Seed Info".
6. Haz click en "Create New Room". Esto te llevara a la pagina del servidor. Pasa el enlace a esta pagina a los
jugadores para que puedan descargar los ficheros de parche de ahi.
**Nota:** Los ficheros de parche de esta pagina permiten a los jugadores conectarse al servidor automaticamente,
mientras que los de la pagina "Seed info" no.
7. Hay un enlace a un MultiWorld Tracker en la parte superior de la pagina de la sala. Deberías pasar también este
enlace a los jugadores para que puedan ver el progreso de la partida. A los observadores también se les puede pasar
este enlace.
8. Una vez todos los jugadores se han unido, podeis empezar a jugar.
## Auto-Tracking
Si deseas usar auto-tracking para tu partida, varios programas ofrecen esta funcionalidad.
El programa recomentdado actualmente es:
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
### Instalación
1. Descarga el fichero de instalacion apropiado para tu ordenador (Usuarios de windows quieren el fichero ".msi").
2. Durante el proceso de insatalación, puede que se te pida instalar Microsoft Visual Studio Build Tools. Un enlace este
programa se muestra durante la proceso, y debe ser ejecutado manualmente.
### Activar auto-tracking
1. Con OpenTracker ejecutado, haz click en el menu Tracking en la parte superior de la ventana, y elige **
AutoTracker...**
2. Click the **Get Devices** button
3. Selecciona tu "SNES device" de la lista
4. Si quieres que las llaves y los objetos de mazmorra tambien sean marcados, activa la caja con nombre **Race Illegal
Tracking**
5. Haz click en el boton **Start Autotracking**
6. Cierra la ventana AutoTracker, ya que deja de ser necesaria

View File

@@ -77,5 +77,5 @@ class TestMiseryMire(TestDungeon):
["Misery Mire - Boss", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], ["Misery Mire - Boss", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Sword', 'Pegasus Boots']], ["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Sword', 'Pegasus Boots']],
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Hammer', 'Pegasus Boots']], ["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Hammer', 'Pegasus Boots']],
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Arrow Upgrade (+5)', 'Pegasus Boots']], ["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Pegasus Boots']],
]) ])

View File

@@ -93,7 +93,7 @@ class AquariaWorld(World):
options: AquariaOptions options: AquariaOptions
"Every options of the world" "Every options of the world"
regions: AquariaRegions | None regions: AquariaRegions
"Used to manage Regions" "Used to manage Regions"
exclude: List[str] exclude: List[str]
@@ -101,17 +101,10 @@ class AquariaWorld(World):
def __init__(self, multiworld: MultiWorld, player: int): def __init__(self, multiworld: MultiWorld, player: int):
"""Initialisation of the Aquaria World""" """Initialisation of the Aquaria World"""
super(AquariaWorld, self).__init__(multiworld, player) super(AquariaWorld, self).__init__(multiworld, player)
self.regions = None self.regions = AquariaRegions(multiworld, player)
self.ingredients_substitution = [] self.ingredients_substitution = []
self.exclude = [] self.exclude = []
def generate_early(self) -> None:
"""
Run before any general steps of the MultiWorld other than options. Useful for getting and adjusting option
results and determining layouts for entrance rando etc. start inventory gets pushed after this step.
"""
self.regions = AquariaRegions(self.multiworld, self.player)
def create_regions(self) -> None: def create_regions(self) -> None:
""" """
Create every Region in `regions` Create every Region in `regions`

View File

@@ -103,9 +103,6 @@ class BlasphemousWorld(World):
if not self.options.wall_climb_shuffle: if not self.options.wall_climb_shuffle:
self.multiworld.push_precollected(self.create_item("Wall Climb Ability")) self.multiworld.push_precollected(self.create_item("Wall Climb Ability"))
if self.options.thorn_shuffle == "local_only":
self.options.local_items.value.add("Thorn Upgrade")
if not self.options.boots_of_pleading: if not self.options.boots_of_pleading:
self.disabled_locations.append("RE401") self.disabled_locations.append("RE401")
@@ -203,6 +200,9 @@ class BlasphemousWorld(World):
if not self.options.skill_randomizer: if not self.options.skill_randomizer:
self.place_items_from_dict(skill_dict) self.place_items_from_dict(skill_dict)
if self.options.thorn_shuffle == "local_only":
self.options.local_items.value.add("Thorn Upgrade")
def place_items_from_set(self, location_set: Set[str], name: str): def place_items_from_set(self, location_set: Set[str], name: str):

View File

@@ -12,12 +12,6 @@
1. Download the above release and extract it. 1. Download the above release and extract it.
## Installation Procedures (Linux and Steam Deck)
1. Download the above release and extract it.
2. Add Celeste64.exe to Steam as a Non-Steam Game. In the properties for it on Steam, set it to use Proton as the compatibility tool. Launch the game through Steam in order to run it.
## Joining a MultiWorld Game ## Joining a MultiWorld Game
1. Before launching the game, edit the `AP.json` file in the root of the Celeste 64 install. 1. Before launching the game, edit the `AP.json` file in the root of the Celeste 64 install.
@@ -39,3 +33,5 @@ An Example `AP.json` file:
"Password": "" "Password": ""
} }
``` ```

View File

@@ -1,4 +1,5 @@
import logging import logging
import asyncio
from NetUtils import ClientStatus, color from NetUtils import ClientStatus, color
from worlds.AutoSNIClient import SNIClient from worlds.AutoSNIClient import SNIClient
@@ -31,7 +32,7 @@ class DKC3SNIClient(SNIClient):
async def validate_rom(self, ctx): async def validate_rom(self, ctx):
from SNIClient import snes_read from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
rom_name = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE) rom_name = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:2] != b"D3": if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:2] != b"D3":

View File

@@ -1,6 +1,6 @@
import typing import typing
from BaseClasses import Item from BaseClasses import Item, ItemClassification
from .Names import ItemName from .Names import ItemName

View File

@@ -1,6 +1,7 @@
from dataclasses import dataclass from dataclasses import dataclass
import typing
from Options import Choice, Range, Toggle, DefaultOnToggle, OptionGroup, PerGameCommonOptions from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
class Goal(Choice): class Goal(Choice):

View File

@@ -1,9 +1,10 @@
import typing import typing
from BaseClasses import Region, Entrance from BaseClasses import MultiWorld, Region, Entrance
from worlds.AutoWorld import World from .Items import DKC3Item
from .Locations import DKC3Location from .Locations import DKC3Location
from .Names import LocationName, ItemName from .Names import LocationName, ItemName
from worlds.AutoWorld import World
def create_regions(world: World, active_locations): def create_regions(world: World, active_locations):

View File

@@ -2,6 +2,7 @@ import Utils
from Utils import read_snes_rom from Utils import read_snes_rom
from worlds.AutoWorld import World from worlds.AutoWorld import World
from worlds.Files import APDeltaPatch from worlds.Files import APDeltaPatch
from .Locations import lookup_id_to_name, all_locations
from .Levels import level_list, level_dict from .Levels import level_list, level_dict
USHASH = '120abf304f0c40fe059f6a192ed4f947' USHASH = '120abf304f0c40fe059f6a192ed4f947'
@@ -435,7 +436,7 @@ level_music_ids = [
class LocalRom: class LocalRom:
def __init__(self, file, name=None, hash=None): def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
self.name = name self.name = name
self.hash = hash self.hash = hash
self.orig_buffer = None self.orig_buffer = None

View File

@@ -1,8 +1,8 @@
import math import math
from worlds.AutoWorld import World
from worlds.generic.Rules import add_rule
from .Names import LocationName, ItemName from .Names import LocationName, ItemName
from worlds.AutoWorld import LogicMixin, World
from worlds.generic.Rules import add_rule, set_rule
def set_rules(world: World): def set_rules(world: World):

View File

@@ -1,13 +1,15 @@
import dataclasses import dataclasses
import math
import os import os
import threading
import typing import typing
import math
import threading
import settings
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from Options import PerGameCommonOptions from Options import PerGameCommonOptions
import Patch
import settings
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from .Client import DKC3SNIClient from .Client import DKC3SNIClient
from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
from .Levels import level_list from .Levels import level_list

View File

@@ -234,7 +234,8 @@ async def game_watcher(ctx: FactorioContext):
f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}") f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
else: else:
data = data["info"] data = data["info"]
research_data: set[int] = {int(tech_name.split("-")[1]) for tech_name in data["research_done"]} research_data = data["research_done"]
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
victory = data["victory"] victory = data["victory"]
await ctx.update_death_link(data["death_link"]) await ctx.update_death_link(data["death_link"])
ctx.multiplayer = data.get("multiplayer", False) ctx.multiplayer = data.get("multiplayer", False)
@@ -248,7 +249,7 @@ async def game_watcher(ctx: FactorioContext):
f"New researches done: " f"New researches done: "
f"{[ctx.location_names.lookup_in_game(rid) for rid in research_data - ctx.locations_checked]}") f"{[ctx.location_names.lookup_in_game(rid) for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data ctx.locations_checked = research_data
await ctx.check_locations(research_data) await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
death_link_tick = data.get("death_link_tick", 0) death_link_tick = data.get("death_link_tick", 0)
if death_link_tick != ctx.death_link_tick: if death_link_tick != ctx.death_link_tick:
ctx.death_link_tick = death_link_tick ctx.death_link_tick = death_link_tick

View File

@@ -3,23 +3,13 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import typing import typing
from schema import Schema, Optional, And, Or, SchemaError from schema import Schema, Optional, And, Or
from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \ from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \
StartInventoryPool, PerGameCommonOptions, OptionGroup StartInventoryPool, PerGameCommonOptions, OptionGroup
# schema helpers # schema helpers
class FloatRange: FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high)
def __init__(self, low, high):
self._low = low
self._high = high
def validate(self, value):
if not isinstance(value, (float, int)):
raise SchemaError(f"should be instance of float or int, but was {value!r}")
if not self._low <= value <= self._high:
raise SchemaError(f"{value} is not between {self._low} and {self._high}")
LuaBool = Or(bool, And(int, lambda n: n in (0, 1))) LuaBool = Or(bool, And(int, lambda n: n in (0, 1)))
@@ -304,11 +294,6 @@ class EvolutionTrapIncrease(Range):
range_end = 100 range_end = 100
class InventorySpillTrapCount(TrapCount):
"""Trap items that when received trigger dropping your main inventory and trash inventory onto the ground."""
display_name = "Inventory Spill Traps"
class FactorioWorldGen(OptionDict): class FactorioWorldGen(OptionDict):
"""World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator, """World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator,
with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings""" with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings"""
@@ -489,7 +474,6 @@ class FactorioOptions(PerGameCommonOptions):
artillery_traps: ArtilleryTrapCount artillery_traps: ArtilleryTrapCount
atomic_rocket_traps: AtomicRocketTrapCount atomic_rocket_traps: AtomicRocketTrapCount
atomic_cliff_remover_traps: AtomicCliffRemoverTrapCount atomic_cliff_remover_traps: AtomicCliffRemoverTrapCount
inventory_spill_traps: InventorySpillTrapCount
attack_traps: AttackTrapCount attack_traps: AttackTrapCount
evolution_traps: EvolutionTrapCount evolution_traps: EvolutionTrapCount
evolution_trap_increase: EvolutionTrapIncrease evolution_trap_increase: EvolutionTrapIncrease
@@ -524,7 +508,6 @@ option_groups: list[OptionGroup] = [
ArtilleryTrapCount, ArtilleryTrapCount,
AtomicRocketTrapCount, AtomicRocketTrapCount,
AtomicCliffRemoverTrapCount, AtomicCliffRemoverTrapCount,
InventorySpillTrapCount,
], ],
start_collapsed=True start_collapsed=True
), ),

View File

@@ -8,7 +8,7 @@ import Utils
import settings import settings
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import Component, components, Type, launch as launch_component from worlds.LauncherComponents import Component, components, Type, launch_subprocess
from worlds.generic import Rules from worlds.generic import Rules
from .Locations import location_pools, location_table from .Locations import location_pools, location_table
from .Mod import generate_mod from .Mod import generate_mod
@@ -24,7 +24,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
def launch_client(): def launch_client():
from .Client import launch from .Client import launch
launch_component(launch, name="FactorioClient") launch_subprocess(launch, name="FactorioClient")
components.append(Component("Factorio Client", "FactorioClient", func=launch_client, component_type=Type.CLIENT)) components.append(Component("Factorio Client", "FactorioClient", func=launch_client, component_type=Type.CLIENT))
@@ -78,7 +78,6 @@ all_items["Cluster Grenade Trap"] = factorio_base_id - 5
all_items["Artillery Trap"] = factorio_base_id - 6 all_items["Artillery Trap"] = factorio_base_id - 6
all_items["Atomic Rocket Trap"] = factorio_base_id - 7 all_items["Atomic Rocket Trap"] = factorio_base_id - 7
all_items["Atomic Cliff Remover Trap"] = factorio_base_id - 8 all_items["Atomic Cliff Remover Trap"] = factorio_base_id - 8
all_items["Inventory Spill Trap"] = factorio_base_id - 9
class Factorio(World): class Factorio(World):
@@ -113,8 +112,6 @@ class Factorio(World):
science_locations: typing.List[FactorioScienceLocation] science_locations: typing.List[FactorioScienceLocation]
removed_technologies: typing.Set[str] removed_technologies: typing.Set[str]
settings: typing.ClassVar[FactorioSettings] settings: typing.ClassVar[FactorioSettings]
trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery",
"Atomic Rocket", "Atomic Cliff Remover", "Inventory Spill")
def __init__(self, world, player: int): def __init__(self, world, player: int):
super(Factorio, self).__init__(world, player) super(Factorio, self).__init__(world, player)
@@ -139,11 +136,15 @@ class Factorio(World):
random = self.random random = self.random
nauvis = Region("Nauvis", player, self.multiworld) nauvis = Region("Nauvis", player, self.multiworld)
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
self.options.evolution_traps + \
for name in self.trap_names: self.options.attack_traps + \
name = name.replace(" ", "_").lower()+"_traps" self.options.teleport_traps + \
location_count += getattr(self.options, name) self.options.grenade_traps + \
self.options.cluster_grenade_traps + \
self.options.atomic_rocket_traps + \
self.options.atomic_cliff_remover_traps + \
self.options.artillery_traps
location_pool = [] location_pool = []
@@ -195,8 +196,9 @@ class Factorio(World):
def create_items(self) -> None: def create_items(self) -> None:
self.custom_technologies = self.set_custom_technologies() self.custom_technologies = self.set_custom_technologies()
self.set_custom_recipes() self.set_custom_recipes()
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket",
for trap_name in self.trap_names: "Atomic Cliff Remover")
for trap_name in traps:
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
range(getattr(self.options, range(getattr(self.options,
f"{trap_name.lower().replace(' ', '_')}_traps"))) f"{trap_name.lower().replace(' ', '_')}_traps")))
@@ -278,6 +280,9 @@ class Factorio(World):
self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player) self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player)
for technology in for technology in
victory_tech_names) victory_tech_names)
for tech_name in victory_tech_names:
if not self.multiworld.get_all_state(True).has(tech_name, player):
print(tech_name)
self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player)
def get_recipe(self, name: str) -> Recipe: def get_recipe(self, name: str) -> Recipe:

View File

@@ -48,40 +48,3 @@ function fire_entity_at_entities(entity_name, entities, speed)
target=target, speed=speed} target=target, speed=speed}
end end
end end
function spill_character_inventory(character)
if not (character and character.valid) then
return false
end
-- grab attrs once pre-loop
local position = character.position
local surface = character.surface
local inventories_to_spill = {
defines.inventory.character_main, -- Main inventory
defines.inventory.character_trash, -- Logistic trash slots
}
for _, inventory_type in pairs(inventories_to_spill) do
local inventory = character.get_inventory(inventory_type)
if inventory and inventory.valid then
-- Spill each item stack onto the ground
for i = 1, #inventory do
local stack = inventory[i]
if stack and stack.valid_for_read then
local spilled_items = surface.spill_item_stack{
position = position,
stack = stack,
enable_looted = false, -- do not mark for auto-pickup
force = nil, -- do not mark for auto-deconstruction
allow_belts = true, -- do mark for putting it onto belts
}
if #spilled_items > 0 then
stack.clear() -- only delete if spilled successfully
end
end
end
end
end
end

View File

@@ -717,10 +717,8 @@ TRAP_TABLE = {
game.surfaces["nauvis"].build_enemy_base(game.forces["player"].get_spawn_position(game.get_surface(1)), 25) game.surfaces["nauvis"].build_enemy_base(game.forces["player"].get_spawn_position(game.get_surface(1)), 25)
end, end,
["Evolution Trap"] = function () ["Evolution Trap"] = function ()
local new_factor = game.forces["enemy"].get_evolution_factor("nauvis") + game.forces["enemy"].evolution_factor = game.forces["enemy"].evolution_factor + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].evolution_factor))
(TRAP_EVO_FACTOR * (1 - game.forces["enemy"].get_evolution_factor("nauvis"))) game.print({"", "New evolution factor:", game.forces["enemy"].evolution_factor})
game.forces["enemy"].set_evolution_factor(new_factor, "nauvis")
game.print({"", "New evolution factor:", new_factor})
end, end,
["Teleport Trap"] = function () ["Teleport Trap"] = function ()
for _, player in ipairs(game.forces["player"].players) do for _, player in ipairs(game.forces["player"].players) do
@@ -750,11 +748,6 @@ end,
fire_entity_at_entities("atomic-rocket", {cliffs[math.random(#cliffs)]}, 0.1) fire_entity_at_entities("atomic-rocket", {cliffs[math.random(#cliffs)]}, 0.1)
end end
end, end,
["Inventory Spill Trap"] = function ()
for _, player in ipairs(game.forces["player"].players) do
spill_character_inventory(player.character)
end
end,
} }
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call) commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)

View File

@@ -1,39 +0,0 @@
"""Tests for error messages from YAML validation."""
import os
import unittest
import WebHostLib.check
FACTORIO_YAML="""
game: Factorio
Factorio:
world_gen:
autoplace_controls:
coal:
richness: 1
frequency: {}
size: 1
"""
def yamlWithFrequency(f):
return FACTORIO_YAML.format(f)
class TestFileValidation(unittest.TestCase):
def test_out_of_range(self):
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1000)})
self.assertIn("between 0 and 6", results["bob.yaml"])
def test_bad_non_numeric(self):
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency("not numeric")})
self.assertIn("float", results["bob.yaml"])
self.assertIn("int", results["bob.yaml"])
def test_good_float(self):
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1.0)})
self.assertIs(results["bob.yaml"], True)
def test_good_int(self):
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1)})
self.assertIs(results["bob.yaml"], True)

View File

@@ -44,13 +44,8 @@ class FaxanaduWorld(World):
location_name_to_id = {loc.name: loc.id for loc in Locations.locations if loc.id is not None} location_name_to_id = {loc.name: loc.id for loc in Locations.locations if loc.id is not None}
def __init__(self, world: MultiWorld, player: int): def __init__(self, world: MultiWorld, player: int):
self.filler_ratios: Dict[str, int] = { self.filler_ratios: Dict[str, int] = {}
item.name: item.count
for item in Items.items
if item.classification in [ItemClassification.filler, ItemClassification.trap]
}
# Remove poison by default to respect itemlinking
self.filler_ratios["Poison"] = 0
super().__init__(world, player) super().__init__(world, player)
def create_regions(self): def create_regions(self):
@@ -165,13 +160,19 @@ class FaxanaduWorld(World):
for i in range(item.progression_count): for i in range(item.progression_count):
itempool.append(FaxanaduItem(item.name, ItemClassification.progression, item.id, self.player)) itempool.append(FaxanaduItem(item.name, ItemClassification.progression, item.id, self.player))
# Adjust filler ratios # Set up filler ratios
self.filler_ratios = {
item.name: item.count
for item in Items.items
if item.classification in [ItemClassification.filler, ItemClassification.trap]
}
# If red potions are locked in shops, remove the count from the ratio. # If red potions are locked in shops, remove the count from the ratio.
self.filler_ratios["Red Potion"] -= red_potion_in_shop_count self.filler_ratios["Red Potion"] -= red_potion_in_shop_count
# Add poisons if desired # Remove poisons if not desired
if self.options.include_poisons: if not self.options.include_poisons:
self.filler_ratios["Poison"] = self.item_name_to_item["Poison"].count self.filler_ratios["Poison"] = 0
# Randomly add fillers to the pool with ratios based on og game occurrence counts. # Randomly add fillers to the pool with ratios based on og game occurrence counts.
filler_count = len(Locations.locations) - len(itempool) - prefilled_count filler_count = len(Locations.locations) - len(itempool) - prefilled_count

View File

@@ -1,4 +1,4 @@
from Options import Choice, FreeText, ItemsAccessibility, Toggle, Range, PerGameCommonOptions from Options import Choice, FreeText, Toggle, Range, PerGameCommonOptions
from dataclasses import dataclass from dataclasses import dataclass
@@ -324,7 +324,6 @@ class KaelisMomFightsMinotaur(Toggle):
@dataclass @dataclass
class FFMQOptions(PerGameCommonOptions): class FFMQOptions(PerGameCommonOptions):
accessibility: ItemsAccessibility
logic: Logic logic: Logic
brown_boxes: BrownBoxes brown_boxes: BrownBoxes
sky_coin_mode: SkyCoinMode sky_coin_mode: SkyCoinMode

View File

@@ -152,23 +152,14 @@ class FFMQWorld(World):
return FFMQItem(name, self.player) return FFMQItem(name, self.player)
def collect_item(self, state, item, remove=False): def collect_item(self, state, item, remove=False):
if not item.advancement:
return None
if "Progressive" in item.name: if "Progressive" in item.name:
i = item.code - 256 i = item.code - 256
if remove:
if state.has(self.item_id_to_name[i+1], self.player):
if state.has(self.item_id_to_name[i+2], self.player):
return self.item_id_to_name[i+2]
return self.item_id_to_name[i+1]
return self.item_id_to_name[i]
if state.has(self.item_id_to_name[i], self.player): if state.has(self.item_id_to_name[i], self.player):
if state.has(self.item_id_to_name[i+1], self.player): if state.has(self.item_id_to_name[i+1], self.player):
return self.item_id_to_name[i+2] return self.item_id_to_name[i+2]
return self.item_id_to_name[i+1] return self.item_id_to_name[i+1]
return self.item_id_to_name[i] return self.item_id_to_name[i]
return item.name return item.name if item.advancement else None
def modify_multidata(self, multidata): def modify_multidata(self, multidata):
# wait for self.rom_name to be available. # wait for self.rom_name to be available.

View File

@@ -333,7 +333,7 @@ class PlandoCharmCosts(OptionDict):
continue continue
try: try:
self.value[key] = CharmCost.from_any(data).value self.value[key] = CharmCost.from_any(data).value
except ValueError: except ValueError as ex:
# will fail schema afterwords # will fail schema afterwords
self.value[key] = data self.value[key] = data

View File

@@ -7,22 +7,22 @@ import itertools
import operator import operator
from collections import defaultdict, Counter from collections import defaultdict, Counter
from .Items import item_table, item_name_groups logger = logging.getLogger("Hollow Knight")
from .Items import item_table, lookup_type_to_names, item_name_groups
from .Regions import create_regions
from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance
from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \
shop_to_option, HKOptions, GrubHuntGoal shop_to_option, HKOptions, GrubHuntGoal
from .ExtractedData import locations, starts, multi_locations, event_names, item_effects, connectors, \ from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
vanilla_shop_costs, vanilla_location_costs event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
from .Charms import names as charm_names from .Charms import names as charm_names
from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, \ from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState
CollectionState
from worlds.AutoWorld import World, LogicMixin, WebWorld from worlds.AutoWorld import World, LogicMixin, WebWorld
from settings import Group, Bool from settings import Group, Bool
logger = logging.getLogger("Hollow Knight")
class HollowKnightSettings(Group): class HollowKnightSettings(Group):
class DisableMapModSpoilers(Bool): class DisableMapModSpoilers(Bool):
@@ -160,7 +160,7 @@ class HKWeb(WebWorld):
class HKWorld(World): class HKWorld(World):
"""Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface, """Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface,
searching for riches, or glory, or answers to old secrets. searching for riches, or glory, or answers to old secrets.
As the enigmatic Knight, youll traverse the depths, unravel its mysteries and conquer its evils. As the enigmatic Knight, youll traverse the depths, unravel its mysteries and conquer its evils.
@@ -181,7 +181,6 @@ class HKWorld(World):
charm_costs: typing.List[int] charm_costs: typing.List[int]
cached_filler_items = {} cached_filler_items = {}
grub_count: int grub_count: int
grub_player_count: typing.Dict[int, int]
def __init__(self, multiworld, player): def __init__(self, multiworld, player):
super(HKWorld, self).__init__(multiworld, player) super(HKWorld, self).__init__(multiworld, player)
@@ -191,6 +190,7 @@ class HKWorld(World):
self.ranges = {} self.ranges = {}
self.created_shop_items = 0 self.created_shop_items = 0
self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
self.grub_count = 0
def generate_early(self): def generate_early(self):
options = self.options options = self.options
@@ -204,14 +204,7 @@ class HKWorld(World):
mini.value = min(mini.value, maxi.value) mini.value = min(mini.value, maxi.value)
self.ranges[term] = mini.value, maxi.value self.ranges[term] = mini.value, maxi.value
self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key], self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key],
True, None, "Event", self.player)) True, None, "Event", self.player))
# defaulting so completion condition isn't incorrect before pre_fill
self.grub_count = (
46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]
else options.GrubHuntGoal.value
)
self.grub_player_count = {self.player: self.grub_count}
def white_palace_exclusions(self): def white_palace_exclusions(self):
exclusions = set() exclusions = set()
@@ -231,6 +224,7 @@ class HKWorld(World):
def create_regions(self): def create_regions(self):
menu_region: Region = create_region(self.multiworld, self.player, 'Menu') menu_region: Region = create_region(self.multiworld, self.player, 'Menu')
self.multiworld.regions.append(menu_region) self.multiworld.regions.append(menu_region)
# wp_exclusions = self.white_palace_exclusions()
# check for any goal that godhome events are relevant to # check for any goal that godhome events are relevant to
all_event_names = event_names.copy() all_event_names = event_names.copy()
@@ -240,17 +234,21 @@ class HKWorld(World):
# Link regions # Link regions
for event_name in sorted(all_event_names): for event_name in sorted(all_event_names):
#if event_name in wp_exclusions:
# continue
loc = HKLocation(self.player, event_name, None, menu_region) loc = HKLocation(self.player, event_name, None, menu_region)
loc.place_locked_item(HKItem(event_name, loc.place_locked_item(HKItem(event_name,
True, True, #event_name not in wp_exclusions,
None, "Event", self.player)) None, "Event", self.player))
menu_region.locations.append(loc) menu_region.locations.append(loc)
for entry_transition, exit_transition in connectors.items(): for entry_transition, exit_transition in connectors.items():
#if entry_transition in wp_exclusions:
# continue
if exit_transition: if exit_transition:
# if door logic fulfilled -> award vanilla target as event # if door logic fulfilled -> award vanilla target as event
loc = HKLocation(self.player, entry_transition, None, menu_region) loc = HKLocation(self.player, entry_transition, None, menu_region)
loc.place_locked_item(HKItem(exit_transition, loc.place_locked_item(HKItem(exit_transition,
True, True, #exit_transition not in wp_exclusions,
None, "Event", self.player)) None, "Event", self.player))
menu_region.locations.append(loc) menu_region.locations.append(loc)
@@ -287,10 +285,7 @@ class HKWorld(World):
if item_name in junk_replace: if item_name in junk_replace:
item_name = self.get_filler_item_name() item_name = self.get_filler_item_name()
item = (self.create_item(item_name) item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name)
if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations
else self.create_event(item_name)
)
if location_name == "Start": if location_name == "Start":
if item_name in randomized_starting_items: if item_name in randomized_starting_items:
@@ -345,8 +340,8 @@ class HKWorld(World):
randomized = True randomized = True
_add("Elevator_Pass", "Elevator_Pass", randomized) _add("Elevator_Pass", "Elevator_Pass", randomized)
for shop, shop_locations in self.created_multi_locations.items(): for shop, locations in self.created_multi_locations.items():
for _ in range(len(shop_locations), getattr(self.options, shop_to_option[shop]).value): for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value):
self.create_location(shop) self.create_location(shop)
unfilled_locations += 1 unfilled_locations += 1
@@ -356,7 +351,7 @@ class HKWorld(World):
# Add additional shop items, as needed. # Add additional shop items, as needed.
if additional_shop_items > 0: if additional_shop_items > 0:
shops = [shop for shop, shop_locations in self.created_multi_locations.items() if len(shop_locations) < 16] shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16)
if not self.options.EggShopSlots: # No eggshop, so don't place items there if not self.options.EggShopSlots: # No eggshop, so don't place items there
shops.remove('Egg_Shop') shops.remove('Egg_Shop')
@@ -378,8 +373,8 @@ class HKWorld(World):
self.sort_shops_by_cost() self.sort_shops_by_cost()
def sort_shops_by_cost(self): def sort_shops_by_cost(self):
for shop, shop_locations in self.created_multi_locations.items(): for shop, locations in self.created_multi_locations.items():
randomized_locations = [loc for loc in shop_locations if not loc.vanilla] randomized_locations = list(loc for loc in locations if not loc.vanilla)
prices = sorted( prices = sorted(
(loc.costs for loc in randomized_locations), (loc.costs for loc in randomized_locations),
key=lambda costs: (len(costs),) + tuple(costs.values()) key=lambda costs: (len(costs),) + tuple(costs.values())
@@ -403,7 +398,7 @@ class HKWorld(World):
return {k: v for k, v in weights.items() if v} return {k: v for k, v in weights.items() if v}
random = self.random random = self.random
hybrid_chance = getattr(self.options, "CostSanityHybridChance").value hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value
weights = { weights = {
data.term: getattr(self.options, f"CostSanity{data.option}Weight").value data.term: getattr(self.options, f"CostSanity{data.option}Weight").value
for data in cost_terms.values() for data in cost_terms.values()
@@ -474,28 +469,29 @@ class HKWorld(World):
elif goal == Goal.option_godhome_flower: elif goal == Goal.option_godhome_flower:
multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player)
elif goal == Goal.option_grub_hunt: elif goal == Goal.option_grub_hunt:
multiworld.completion_condition[player] = lambda state: self.can_grub_goal(state) pass # will set in stage_pre_fill()
else: else:
# Any goal # Any goal
multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) and \ multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) and \
_hk_can_beat_radiance(state, player) and state.count("Godhome_Flower_Quest", player) and \ _hk_can_beat_radiance(state, player) and state.count("Godhome_Flower_Quest", player)
self.can_grub_goal(state)
set_rules(self) set_rules(self)
def can_grub_goal(self, state: CollectionState) -> bool:
return all(state.has("Grub", owner, count) for owner, count in self.grub_player_count.items())
@classmethod @classmethod
def stage_pre_fill(cls, multiworld: "MultiWorld"): def stage_pre_fill(cls, multiworld: "MultiWorld"):
def set_goal(player, grub_rule: typing.Callable[[CollectionState], bool]):
world = multiworld.worlds[player]
if world.options.Goal == "grub_hunt":
multiworld.completion_condition[player] = grub_rule
else:
old_rule = multiworld.completion_condition[player]
multiworld.completion_condition[player] = lambda state: old_rule(state) and grub_rule(state)
worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]] worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]]
if worlds: if worlds:
grubs = [item for item in multiworld.get_items() if item.name == "Grub"] grubs = [item for item in multiworld.get_items() if item.name == "Grub"]
all_grub_players = [ all_grub_players = [world.player for world in worlds if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]]
world.player
for world in worlds
if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]
]
if all_grub_players: if all_grub_players:
group_lookup = defaultdict(set) group_lookup = defaultdict(set)
@@ -529,13 +525,13 @@ class HKWorld(World):
for player, grub_player_count in per_player_grubs_per_player.items(): for player, grub_player_count in per_player_grubs_per_player.items():
if player in all_grub_players: if player in all_grub_players:
multiworld.worlds[player].grub_player_count = grub_player_count set_goal(player, lambda state, g=grub_player_count: all(state.has("Grub", owner, count) for owner, count in g.items()))
for world in worlds: for world in worlds:
if world.player not in all_grub_players: if world.player not in all_grub_players:
world.grub_count = world.options.GrubHuntGoal.value world.grub_count = world.options.GrubHuntGoal.value
player = world.player player = world.player
world.grub_player_count = {player: world.grub_count} set_goal(player, lambda state, p=player, c=world.grub_count: state.has("Grub", p, c))
def fill_slot_data(self): def fill_slot_data(self):
slot_data = {} slot_data = {}
@@ -670,8 +666,8 @@ class HKWorld(World):
): ):
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}") spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
else: else:
for shop_name, shop_locations in hk_world.created_multi_locations.items(): for shop_name, locations in hk_world.created_multi_locations.items():
for loc in shop_locations: for loc in locations:
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}") spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str: def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str:

View File

@@ -2,6 +2,7 @@ import typing
from argparse import Namespace from argparse import Namespace
from BaseClasses import CollectionState, MultiWorld from BaseClasses import CollectionState, MultiWorld
from Options import ItemLinks from Options import ItemLinks
from test.bases import WorldTestBase
from worlds.AutoWorld import AutoWorldRegister, call_all from worlds.AutoWorld import AutoWorldRegister, call_all
from .. import HKWorld from .. import HKWorld

View File

@@ -1,6 +1,5 @@
from test.bases import WorldTestBase from . import linkedTestHK, WorldTestBase
from Options import ItemLinks from Options import ItemLinks
from . import linkedTestHK
class test_grubcount_limited(linkedTestHK, WorldTestBase): class test_grubcount_limited(linkedTestHK, WorldTestBase):

View File

@@ -206,19 +206,19 @@ def set_rules(world: "KDL3World") -> None:
lambda state: can_reach_needle(state, world.player)) lambda state: can_reach_needle(state, world.player))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u2, world.player), set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u2, world.player),
lambda state: can_reach_ice(state, world.player) and lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player) (can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player))) or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u3, world.player), set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u3, world.player),
lambda state: can_reach_ice(state, world.player) and lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player) (can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player))) or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u4, world.player), set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u4, world.player),
lambda state: can_reach_ice(state, world.player) and lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player) (can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player))) or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.cloudy_park_6_u1, world.player), set_rule(world.multiworld.get_location(location_name.cloudy_park_6_u1, world.player),
lambda state: can_reach_cutter(state, world.player)) lambda state: can_reach_cutter(state, world.player))
@@ -248,9 +248,9 @@ def set_rules(world: "KDL3World") -> None:
for i in range(12, 18): for i in range(12, 18):
set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player), set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
lambda state: can_reach_ice(state, world.player) and lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player) (can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player))) or can_reach_nago(state, world.player)))
for i in range(21, 23): for i in range(21, 23):
set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player), set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
lambda state: can_reach_chuchu(state, world.player)) lambda state: can_reach_chuchu(state, world.player))
@@ -307,7 +307,7 @@ def set_rules(world: "KDL3World") -> None:
lambda state: can_reach_coo(state, world.player) and can_reach_burning(state, world.player)) lambda state: can_reach_coo(state, world.player) and can_reach_burning(state, world.player))
set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a3, world.player), set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a3, world.player),
lambda state: can_reach_chuchu(state, world.player) and can_reach_coo(state, world.player) lambda state: can_reach_chuchu(state, world.player) and can_reach_coo(state, world.player)
and can_reach_burning(state, world.player)) and can_reach_burning(state, world.player))
for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified", for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified",
"Level 3 Boss - Purified", "Level 4 Boss - Purified", "Level 3 Boss - Purified", "Level 4 Boss - Purified",
@@ -329,14 +329,6 @@ def set_rules(world: "KDL3World") -> None:
world.options.ow_boss_requirement.value, world.options.ow_boss_requirement.value,
world.player_levels))) world.player_levels)))
if world.options.open_world:
for boss_flag, level in zip(["Level 1 Boss - Defeated", "Level 2 Boss - Defeated", "Level 3 Boss - Defeated",
"Level 4 Boss - Defeated", "Level 5 Boss - Defeated"],
location_name.level_names.keys()):
set_rule(world.get_location(boss_flag),
lambda state, lvl=level: state.has(f"{lvl} - Stage Completion", world.player,
world.options.ow_boss_requirement.value))
set_rule(world.multiworld.get_entrance("To Level 6", world.player), set_rule(world.multiworld.get_entrance("To Level 6", world.player),
lambda state: state.has("Heart Star", world.player, world.required_heart_stars)) lambda state: state.has("Heart Star", world.player, world.required_heart_stars))

View File

@@ -483,8 +483,6 @@ def create_regions(multiworld: MultiWorld, player: int, options):
for name, data in regions.items(): for name, data in regions.items():
multiworld.regions.append(create_region(multiworld, player, name, data)) multiworld.regions.append(create_region(multiworld, player, name, data))
def connect_entrances(multiworld: MultiWorld, player: int):
multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player)) multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player))
multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player)) multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player))
multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player)) multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player))
@@ -502,7 +500,6 @@ def connect_entrances(multiworld: MultiWorld, player: int):
multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player)) multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player))
multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player)) multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player))
def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData): def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData):
region = Region(name, player, multiworld) region = Region(name, player, multiworld)
if data.locations: if data.locations:

View File

@@ -6,15 +6,15 @@ from worlds.AutoWorld import WebWorld, World
from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups
from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups
from .Options import KH1Options, kh1_option_groups from .Options import KH1Options, kh1_option_groups
from .Regions import connect_entrances, create_regions from .Regions import create_regions
from .Rules import set_rules from .Rules import set_rules
from .Presets import kh1_option_presets from .Presets import kh1_option_presets
from worlds.LauncherComponents import Component, components, Type, launch as launch_component from worlds.LauncherComponents import Component, components, Type, launch_subprocess
def launch_client(): def launch_client():
from .Client import launch from .Client import launch
launch_component(launch, name="KH1 Client") launch_subprocess(launch, name="KH1 Client")
components.append(Component("KH1 Client", "KH1Client", func=launch_client, component_type=Type.CLIENT)) components.append(Component("KH1 Client", "KH1Client", func=launch_client, component_type=Type.CLIENT))
@@ -242,9 +242,6 @@ class KH1World(World):
def create_regions(self): def create_regions(self):
create_regions(self.multiworld, self.player, self.options) create_regions(self.multiworld, self.player, self.options)
def connect_entrances(self):
connect_entrances(self.multiworld, self.player)
def generate_early(self): def generate_early(self):
value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"] value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"]

View File

@@ -5,10 +5,8 @@ ModuleUpdate.update()
import os import os
import asyncio import asyncio
import json import json
import requests
from pymem import pymem from pymem import pymem
from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, \ from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, SupportAbility_Table, ActionAbility_Table, all_weapon_slot
SupportAbility_Table, ActionAbility_Table, all_weapon_slot
from .Names import ItemName from .Names import ItemName
from .WorldLocations import * from .WorldLocations import *
@@ -84,7 +82,6 @@ class KH2Context(CommonContext):
} }
self.kh2seedname = None self.kh2seedname = None
self.kh2slotdata = None self.kh2slotdata = None
self.mem_json = None
self.itemamount = {} self.itemamount = {}
if "localappdata" in os.environ: if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP") self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
@@ -113,7 +110,6 @@ class KH2Context(CommonContext):
18: TWTNW_Checks, 18: TWTNW_Checks,
# 255: {}, # starting screen # 255: {}, # starting screen
} }
self.last_world_int = -1
# 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room # 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room
# self.sveroom = 0x2A09C00 + 0x41 # self.sveroom = 0x2A09C00 + 0x41
# 0 not in battle 1 in yellow battle 2 red battle #short # 0 not in battle 1 in yellow battle 2 red battle #short
@@ -181,8 +177,7 @@ class KH2Context(CommonContext):
self.base_accessory_slots = 1 self.base_accessory_slots = 1
self.base_armor_slots = 1 self.base_armor_slots = 1
self.base_item_slots = 3 self.base_item_slots = 3
self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772]
0x2770, 0x2772]
async def server_auth(self, password_requested: bool = False): async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password: if password_requested and not self.password:
@@ -344,14 +339,39 @@ class KH2Context(CommonContext):
self.locations_checked |= new_locations self.locations_checked |= new_locations
if cmd in {"DataPackage"}: if cmd in {"DataPackage"}:
if "Kingdom Hearts 2" in args["data"]["games"]: self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"]
self.data_package_kh2_cache(args) self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
if "KeybladeAbilities" in self.kh2slotdata.keys(): self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"]
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
if "keyblade_abilities" in self.kh2slotdata.keys():
sora_ability_dict = self.kh2slotdata["KeybladeAbilities"]
# sora ability to slot # sora ability to slot
self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"])
# itemid:[slots that are available for that item] # itemid:[slots that are available for that item]
self.AbilityQuantityDict.update(self.kh2slotdata["StaffAbilities"]) for k, v in sora_ability_dict.items():
self.AbilityQuantityDict.update(self.kh2slotdata["ShieldAbilities"]) if v >= 1:
if k not in self.sora_ability_to_slot.keys():
self.sora_ability_to_slot[k] = []
for _ in range(sora_ability_dict[k]):
self.sora_ability_to_slot[k].append(self.kh2_seed_save_cache["SoraInvo"][0])
self.kh2_seed_save_cache["SoraInvo"][0] -= 2
donald_ability_dict = self.kh2slotdata["StaffAbilities"]
for k, v in donald_ability_dict.items():
if v >= 1:
if k not in self.donald_ability_to_slot.keys():
self.donald_ability_to_slot[k] = []
for _ in range(donald_ability_dict[k]):
self.donald_ability_to_slot[k].append(self.kh2_seed_save_cache["DonaldInvo"][0])
self.kh2_seed_save_cache["DonaldInvo"][0] -= 2
goofy_ability_dict = self.kh2slotdata["ShieldAbilities"]
for k, v in goofy_ability_dict.items():
if v >= 1:
if k not in self.goofy_ability_to_slot.keys():
self.goofy_ability_to_slot[k] = []
for _ in range(goofy_ability_dict[k]):
self.goofy_ability_to_slot[k].append(self.kh2_seed_save_cache["GoofyInvo"][0])
self.kh2_seed_save_cache["GoofyInvo"][0] -= 2
all_weapon_location_id = [] all_weapon_location_id = []
for weapon_location in all_weapon_slot: for weapon_location in all_weapon_slot:
@@ -359,9 +379,24 @@ class KH2Context(CommonContext):
self.all_weapon_location_id = set(all_weapon_location_id) self.all_weapon_location_id = set(all_weapon_location_id)
try: try:
if not self.kh2: self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") if self.kh2_game_version is None:
self.get_addresses() if self.kh2_read_string(0x09A9830, 4) == "KH2J":
self.kh2_game_version = "STEAM"
self.Now = 0x0717008
self.Save = 0x09A9830
self.Slot1 = 0x2A23518
self.Journal = 0x7434E0
self.Shop = 0x7435D0
elif self.kh2_read_string(0x09A92F0, 4) == "KH2J":
self.kh2_game_version = "EGS"
else:
self.kh2_game_version = None
logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
if self.kh2_game_version is not None:
logger.info(f"You are now auto-tracking. {self.kh2_game_version}")
self.kh2connected = True
except Exception as e: except Exception as e:
if self.kh2connected: if self.kh2connected:
@@ -370,25 +405,16 @@ class KH2Context(CommonContext):
self.serverconneced = True self.serverconneced = True
asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}])) asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}]))
def data_package_kh2_cache(self, args):
self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"]
self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"]
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
async def checkWorldLocations(self): async def checkWorldLocations(self):
try: try:
currentworldint = self.kh2_read_byte(self.Now) currentworldint = self.kh2_read_byte(self.Now)
if self.last_world_int != currentworldint: await self.send_msgs([{
self.last_world_int = currentworldint "cmd": "Set", "key": "Slot: " + str(self.slot) + " :CurrentWorld",
await self.send_msgs([{ "default": 0, "want_reply": True, "operations": [{
"cmd": "Set", "key": "Slot: " + str(self.slot) + " :CurrentWorld", "operation": "replace",
"default": 0, "want_reply": False, "operations": [{ "value": currentworldint
"operation": "replace", }]
"value": currentworldint }])
}]
}])
if currentworldint in self.worldid_to_locations: if currentworldint in self.worldid_to_locations:
curworldid = self.worldid_to_locations[currentworldint] curworldid = self.worldid_to_locations[currentworldint]
for location, data in curworldid.items(): for location, data in curworldid.items():
@@ -417,6 +443,7 @@ class KH2Context(CommonContext):
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels], 0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels], 5: ["SummonLevel", SummonLevels] 3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels], 5: ["SummonLevel", SummonLevels]
} }
# TODO: remove formDict[i][0] in self.kh2_seed_save_cache["Levels"].keys() after 4.3
for i in range(6): for i in range(6):
for location, data in formDict[i][1].items(): for location, data in formDict[i][1].items():
formlevel = self.kh2_read_byte(self.Save + data.addrObtained) formlevel = self.kh2_read_byte(self.Save + data.addrObtained)
@@ -460,11 +487,9 @@ class KH2Context(CommonContext):
if locationName in self.chest_set: if locationName in self.chest_set:
if locationName in self.location_name_to_worlddata.keys(): if locationName in self.location_name_to_worlddata.keys():
locationData = self.location_name_to_worlddata[locationName] locationData = self.location_name_to_worlddata[locationName]
if self.kh2_read_byte( if self.kh2_read_byte(self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0:
self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0:
roomData = self.kh2_read_byte(self.Save + locationData.addrObtained) roomData = self.kh2_read_byte(self.Save + locationData.addrObtained)
self.kh2_write_byte(self.Save + locationData.addrObtained, self.kh2_write_byte(self.Save + locationData.addrObtained, roomData | 0x01 << locationData.bitIndex)
roomData | 0x01 << locationData.bitIndex)
except Exception as e: except Exception as e:
if self.kh2connected: if self.kh2connected:
@@ -487,9 +512,6 @@ class KH2Context(CommonContext):
async def give_item(self, item, location): async def give_item(self, item, location):
try: try:
# todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites
#sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
while not self.lookup_id_to_item:
await asyncio.sleep(0.5)
itemname = self.lookup_id_to_item[item] itemname = self.lookup_id_to_item[item]
itemdata = self.item_name_to_data[itemname] itemdata = self.item_name_to_data[itemname]
# itemcode = self.kh2_item_name_to_id[itemname] # itemcode = self.kh2_item_name_to_id[itemname]
@@ -503,7 +525,27 @@ class KH2Context(CommonContext):
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]: if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]:
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = [] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = []
# appending the slot that the ability should be in # appending the slot that the ability should be in
if len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ # for non beta. remove after 4.3
if "PoptrackerVersion" in self.kh2slotdata:
if self.kh2slotdata["PoptrackerVersionCheck"] < 4.3:
if (itemname in self.sora_ability_set
and len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < self.item_name_to_data[itemname].quantity) \
and self.kh2_seed_save_cache["SoraInvo"][1] > 0x254C:
ability_slot = self.kh2_seed_save_cache["SoraInvo"][1]
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
self.kh2_seed_save_cache["SoraInvo"][1] -= 2
elif itemname in self.donald_ability_set:
ability_slot = self.kh2_seed_save_cache["DonaldInvo"][1]
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
self.kh2_seed_save_cache["DonaldInvo"][1] -= 2
else:
ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1]
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
self.kh2_seed_save_cache["GoofyInvo"][1] -= 2
if ability_slot in self.front_ability_slots:
self.front_ability_slots.remove(ability_slot)
elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
self.AbilityQuantityDict[itemname]: self.AbilityQuantityDict[itemname]:
if itemname in self.sora_ability_set: if itemname in self.sora_ability_set:
ability_slot = self.kh2_seed_save_cache["SoraInvo"][0] ability_slot = self.kh2_seed_save_cache["SoraInvo"][0]
@@ -633,8 +675,7 @@ class KH2Context(CommonContext):
item_data = self.item_name_to_data[item_name] item_data = self.item_name_to_data[item_name]
# if the inventory slot for that keyblade is less than the amount they should have, # if the inventory slot for that keyblade is less than the amount they should have,
# and they are not in stt # and they are not in stt
if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte( if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(self.Save + 0x1CFF) != 13:
self.Save + 0x1CFF) != 13:
# Checking form anchors for the keyblade to remove extra keyblades # Checking form anchors for the keyblade to remove extra keyblades
if self.kh2_read_short(self.Save + 0x24F0) == item_data.kh2id \ if self.kh2_read_short(self.Save + 0x24F0) == item_data.kh2id \
or self.kh2_read_short(self.Save + 0x32F4) == item_data.kh2id \ or self.kh2_read_short(self.Save + 0x32F4) == item_data.kh2id \
@@ -735,8 +776,7 @@ class KH2Context(CommonContext):
item_data = self.item_name_to_data[item_name] item_data = self.item_name_to_data[item_name]
amount_of_items = 0 amount_of_items = 0
amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name] amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name]
if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte( if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(self.Shop) in {10, 8}:
self.Shop) in {10, 8}:
self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
for item_name in master_stat: for item_name in master_stat:
@@ -795,8 +835,7 @@ class KH2Context(CommonContext):
# self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) # self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
if "PoptrackerVersionCheck" in self.kh2slotdata: if "PoptrackerVersionCheck" in self.kh2slotdata:
if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte( if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3
self.Save + 0x3607) != 1: # telling the goa they are on version 4.3
self.kh2_write_byte(self.Save + 0x3607, 1) self.kh2_write_byte(self.Save + 0x3607, 1)
except Exception as e: except Exception as e:
@@ -805,59 +844,10 @@ class KH2Context(CommonContext):
logger.info(e) logger.info(e)
logger.info("line 840") logger.info("line 840")
def get_addresses(self):
if not self.kh2connected and self.kh2 is not None:
if self.kh2_game_version is None:
if self.kh2_read_string(0x09A9830, 4) == "KH2J": def finishedGame(ctx: KH2Context, message):
self.kh2_game_version = "STEAM"
self.Now = 0x0717008
self.Save = 0x09A9830
self.Slot1 = 0x2A23518
self.Journal = 0x7434E0
self.Shop = 0x7435D0
elif self.kh2_read_string(0x09A92F0, 4) == "KH2J":
self.kh2_game_version = "EGS"
else:
if self.game_communication_path:
logger.info("Checking with most up to date addresses of github. If file is not found will be downloading datafiles. This might take a moment")
#if mem addresses file is found then check version and if old get new one
kh2memaddresses_path = os.path.join(self.game_communication_path, f"kh2memaddresses.json")
if not os.path.exists(kh2memaddresses_path):
mem_resp = requests.get("https://raw.githubusercontent.com/JaredWeakStrike/KH2APMemoryValues/master/kh2memaddresses.json")
if mem_resp.status_code == 200:
self.mem_json = json.loads(mem_resp.content)
with open(kh2memaddresses_path,
'w') as f:
f.write(json.dumps(self.mem_json, indent=4))
else:
with open(kh2memaddresses_path, 'r') as f:
self.mem_json = json.load(f)
if self.mem_json:
for key in self.mem_json.keys():
if self.kh2_read_string(int(self.mem_json[key]["GameVersionCheck"], 0), 4) == "KH2J":
self.Now = int(self.mem_json[key]["Now"], 0)
self.Save = int(self.mem_json[key]["Save"], 0)
self.Slot1 = int(self.mem_json[key]["Slot1"], 0)
self.Journal = int(self.mem_json[key]["Journal"], 0)
self.Shop = int(self.mem_json[key]["Shop"], 0)
self.kh2_game_version = key
if self.kh2_game_version is not None:
logger.info(f"You are now auto-tracking {self.kh2_game_version}")
self.kh2connected = True
else:
logger.info("Your game version does not match what the client requires. Check in the "
"kingdom-hearts-2-final-mix channel for more information on correcting the game "
"version.")
self.kh2connected = False
def finishedGame(ctx: KH2Context):
if ctx.kh2slotdata['FinalXemnas'] == 1: if ctx.kh2slotdata['FinalXemnas'] == 1:
if not ctx.final_xemnas and ctx.kh2_read_byte( if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
& 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0: & 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0:
ctx.final_xemnas = True ctx.final_xemnas = True
# three proofs # three proofs
@@ -887,12 +877,10 @@ def finishedGame(ctx: KH2Context):
elif ctx.kh2slotdata['Goal'] == 2: elif ctx.kh2slotdata['Goal'] == 2:
# for backwards compat # for backwards compat
if "hitlist" in ctx.kh2slotdata: if "hitlist" in ctx.kh2slotdata:
locations = ctx.sending
for boss in ctx.kh2slotdata["hitlist"]: for boss in ctx.kh2slotdata["hitlist"]:
if boss in locations: if boss in message[0]["locations"]:
ctx.hitlist_bounties += 1 ctx.hitlist_bounties += 1
if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"][ if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"]:
"Bounty"] >= ctx.kh2slotdata["BountyRequired"]:
if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1: if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1:
ctx.kh2_write_byte(ctx.Save + 0x36B2, 1) ctx.kh2_write_byte(ctx.Save + 0x36B2, 1)
ctx.kh2_write_byte(ctx.Save + 0x36B3, 1) ctx.kh2_write_byte(ctx.Save + 0x36B3, 1)
@@ -931,19 +919,35 @@ async def kh2_watcher(ctx: KH2Context):
await asyncio.create_task(ctx.verifyChests()) await asyncio.create_task(ctx.verifyChests())
await asyncio.create_task(ctx.verifyItems()) await asyncio.create_task(ctx.verifyItems())
await asyncio.create_task(ctx.verifyLevel()) await asyncio.create_task(ctx.verifyLevel())
if finishedGame(ctx) and not ctx.kh2_finished_game: message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
if finishedGame(ctx, message) and not ctx.kh2_finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.kh2_finished_game = True ctx.kh2_finished_game = True
if ctx.sending: await ctx.send_msgs(message)
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
await ctx.send_msgs(message)
elif not ctx.kh2connected and ctx.serverconneced: elif not ctx.kh2connected and ctx.serverconneced:
logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.") logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.")
ctx.kh2 = None ctx.kh2 = None
while not ctx.kh2connected and ctx.serverconneced: while not ctx.kh2connected and ctx.serverconneced:
await asyncio.sleep(15) await asyncio.sleep(15)
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
ctx.get_addresses() if ctx.kh2 is not None:
if ctx.kh2_game_version is None:
if ctx.kh2_read_string(0x09A9830, 4) == "KH2J":
ctx.kh2_game_version = "STEAM"
ctx.Now = 0x0717008
ctx.Save = 0x09A9830
ctx.Slot1 = 0x2A23518
ctx.Journal = 0x7434E0
ctx.Shop = 0x7435D0
elif ctx.kh2_read_string(0x09A92F0, 4) == "KH2J":
ctx.kh2_game_version = "EGS"
else:
ctx.kh2_game_version = None
logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
if ctx.kh2_game_version is not None:
logger.info(f"You are now auto-tracking {ctx.kh2_game_version}")
ctx.kh2connected = True
except Exception as e: except Exception as e:
if ctx.kh2connected: if ctx.kh2connected:
ctx.kh2connected = False ctx.kh2connected = False

View File

@@ -540,7 +540,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = {
LocationName.SephirothFenrir, LocationName.SephirothFenrir,
LocationName.SephiEventLocation LocationName.SephiEventLocation
], ],
RegionName.CoR: [ #todo: make logic for getting these checks. RegionName.CoR: [
LocationName.CoRDepthsAPBoost, LocationName.CoRDepthsAPBoost,
LocationName.CoRDepthsPowerCrystal, LocationName.CoRDepthsPowerCrystal,
LocationName.CoRDepthsFrostCrystal, LocationName.CoRDepthsFrostCrystal,
@@ -1032,99 +1032,99 @@ def connect_regions(self):
multiworld = self.multiworld multiworld = self.multiworld
player = self.player player = self.player
# connecting every first visit to the GoA # connecting every first visit to the GoA
KH2RegionConnections: typing.Dict[str, typing.Tuple[str]] = { KH2RegionConnections: typing.Dict[str, typing.Set[str]] = {
"Menu": (RegionName.GoA,), "Menu": {RegionName.GoA},
RegionName.GoA: (RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht, RegionName.GoA: {RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht,
RegionName.LoD, RegionName.LoD,
RegionName.Twtnw, RegionName.Bc, RegionName.Ag, RegionName.Pl, RegionName.Hb, RegionName.Twtnw, RegionName.Bc, RegionName.Ag, RegionName.Pl, RegionName.Hb,
RegionName.Dc, RegionName.Stt, RegionName.Dc, RegionName.Stt,
RegionName.Ha1, RegionName.Keyblade, RegionName.LevelsVS1, RegionName.Ha1, RegionName.Keyblade, RegionName.LevelsVS1,
RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master, RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master,
RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne), RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne},
RegionName.LoD: (RegionName.ShanYu,), RegionName.LoD: {RegionName.ShanYu},
RegionName.ShanYu: (RegionName.LoD2,), RegionName.ShanYu: {RegionName.LoD2},
RegionName.LoD2: (RegionName.AnsemRiku,), RegionName.LoD2: {RegionName.AnsemRiku},
RegionName.AnsemRiku: (RegionName.StormRider,), RegionName.AnsemRiku: {RegionName.StormRider},
RegionName.StormRider: (RegionName.DataXigbar,), RegionName.StormRider: {RegionName.DataXigbar},
RegionName.Ag: (RegionName.TwinLords,), RegionName.Ag: {RegionName.TwinLords},
RegionName.TwinLords: (RegionName.Ag2,), RegionName.TwinLords: {RegionName.Ag2},
RegionName.Ag2: (RegionName.GenieJafar,), RegionName.Ag2: {RegionName.GenieJafar},
RegionName.GenieJafar: (RegionName.DataLexaeus,), RegionName.GenieJafar: {RegionName.DataLexaeus},
RegionName.Dc: (RegionName.Tr,), RegionName.Dc: {RegionName.Tr},
RegionName.Tr: (RegionName.OldPete,), RegionName.Tr: {RegionName.OldPete},
RegionName.OldPete: (RegionName.FuturePete,), RegionName.OldPete: {RegionName.FuturePete},
RegionName.FuturePete: (RegionName.Terra, RegionName.DataMarluxia), RegionName.FuturePete: {RegionName.Terra, RegionName.DataMarluxia},
RegionName.Ha1: (RegionName.Ha2,), RegionName.Ha1: {RegionName.Ha2},
RegionName.Ha2: (RegionName.Ha3,), RegionName.Ha2: {RegionName.Ha3},
RegionName.Ha3: (RegionName.Ha4,), RegionName.Ha3: {RegionName.Ha4},
RegionName.Ha4: (RegionName.Ha5,), RegionName.Ha4: {RegionName.Ha5},
RegionName.Ha5: (RegionName.Ha6,), RegionName.Ha5: {RegionName.Ha6},
RegionName.Pr: (RegionName.Barbosa,), RegionName.Pr: {RegionName.Barbosa},
RegionName.Barbosa: (RegionName.Pr2,), RegionName.Barbosa: {RegionName.Pr2},
RegionName.Pr2: (RegionName.GrimReaper1,), RegionName.Pr2: {RegionName.GrimReaper1},
RegionName.GrimReaper1: (RegionName.GrimReaper2,), RegionName.GrimReaper1: {RegionName.GrimReaper2},
RegionName.GrimReaper2: (RegionName.DataLuxord,), RegionName.GrimReaper2: {RegionName.DataLuxord},
RegionName.Oc: (RegionName.Cerberus,), RegionName.Oc: {RegionName.Cerberus},
RegionName.Cerberus: (RegionName.OlympusPete,), RegionName.Cerberus: {RegionName.OlympusPete},
RegionName.OlympusPete: (RegionName.Hydra,), RegionName.OlympusPete: {RegionName.Hydra},
RegionName.Hydra: (RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2), RegionName.Hydra: {RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2},
RegionName.Oc2: (RegionName.Hades,), RegionName.Oc2: {RegionName.Hades},
RegionName.Hades: (RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion), RegionName.Hades: {RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion},
RegionName.Oc2GofCup: (RegionName.HadesCups,), RegionName.Oc2GofCup: {RegionName.HadesCups},
RegionName.Bc: (RegionName.Thresholder,), RegionName.Bc: {RegionName.Thresholder},
RegionName.Thresholder: (RegionName.Beast,), RegionName.Thresholder: {RegionName.Beast},
RegionName.Beast: (RegionName.DarkThorn,), RegionName.Beast: {RegionName.DarkThorn},
RegionName.DarkThorn: (RegionName.Bc2,), RegionName.DarkThorn: {RegionName.Bc2},
RegionName.Bc2: (RegionName.Xaldin,), RegionName.Bc2: {RegionName.Xaldin},
RegionName.Xaldin: (RegionName.DataXaldin,), RegionName.Xaldin: {RegionName.DataXaldin},
RegionName.Sp: (RegionName.HostileProgram,), RegionName.Sp: {RegionName.HostileProgram},
RegionName.HostileProgram: (RegionName.Sp2,), RegionName.HostileProgram: {RegionName.Sp2},
RegionName.Sp2: (RegionName.Mcp,), RegionName.Sp2: {RegionName.Mcp},
RegionName.Mcp: (RegionName.DataLarxene,), RegionName.Mcp: {RegionName.DataLarxene},
RegionName.Ht: (RegionName.PrisonKeeper,), RegionName.Ht: {RegionName.PrisonKeeper},
RegionName.PrisonKeeper: (RegionName.OogieBoogie,), RegionName.PrisonKeeper: {RegionName.OogieBoogie},
RegionName.OogieBoogie: (RegionName.Ht2,), RegionName.OogieBoogie: {RegionName.Ht2},
RegionName.Ht2: (RegionName.Experiment,), RegionName.Ht2: {RegionName.Experiment},
RegionName.Experiment: (RegionName.DataVexen,), RegionName.Experiment: {RegionName.DataVexen},
RegionName.Hb: (RegionName.Hb2,), RegionName.Hb: {RegionName.Hb2},
RegionName.Hb2: (RegionName.CoR, RegionName.HBDemyx), RegionName.Hb2: {RegionName.CoR, RegionName.HBDemyx},
RegionName.HBDemyx: (RegionName.ThousandHeartless,), RegionName.HBDemyx: {RegionName.ThousandHeartless},
RegionName.ThousandHeartless: (RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi), RegionName.ThousandHeartless: {RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi},
RegionName.CoR: (RegionName.CorFirstFight,), RegionName.CoR: {RegionName.CorFirstFight},
RegionName.CorFirstFight: (RegionName.CorSecondFight,), RegionName.CorFirstFight: {RegionName.CorSecondFight},
RegionName.CorSecondFight: (RegionName.Transport,), RegionName.CorSecondFight: {RegionName.Transport},
RegionName.Pl: (RegionName.Scar,), RegionName.Pl: {RegionName.Scar},
RegionName.Scar: (RegionName.Pl2,), RegionName.Scar: {RegionName.Pl2},
RegionName.Pl2: (RegionName.GroundShaker,), RegionName.Pl2: {RegionName.GroundShaker},
RegionName.GroundShaker: (RegionName.DataSaix,), RegionName.GroundShaker: {RegionName.DataSaix},
RegionName.Stt: (RegionName.TwilightThorn,), RegionName.Stt: {RegionName.TwilightThorn},
RegionName.TwilightThorn: (RegionName.Axel1,), RegionName.TwilightThorn: {RegionName.Axel1},
RegionName.Axel1: (RegionName.Axel2,), RegionName.Axel1: {RegionName.Axel2},
RegionName.Axel2: (RegionName.DataRoxas,), RegionName.Axel2: {RegionName.DataRoxas},
RegionName.Tt: (RegionName.Tt2,), RegionName.Tt: {RegionName.Tt2},
RegionName.Tt2: (RegionName.Tt3,), RegionName.Tt2: {RegionName.Tt3},
RegionName.Tt3: (RegionName.DataAxel,), RegionName.Tt3: {RegionName.DataAxel},
RegionName.Twtnw: (RegionName.Roxas,), RegionName.Twtnw: {RegionName.Roxas},
RegionName.Roxas: (RegionName.Xigbar,), RegionName.Roxas: {RegionName.Xigbar},
RegionName.Xigbar: (RegionName.Luxord,), RegionName.Xigbar: {RegionName.Luxord},
RegionName.Luxord: (RegionName.Saix,), RegionName.Luxord: {RegionName.Saix},
RegionName.Saix: (RegionName.Twtnw2,), RegionName.Saix: {RegionName.Twtnw2},
RegionName.Twtnw2: (RegionName.Xemnas,), RegionName.Twtnw2: {RegionName.Xemnas},
RegionName.Xemnas: (RegionName.ArmoredXemnas, RegionName.DataXemnas), RegionName.Xemnas: {RegionName.ArmoredXemnas, RegionName.DataXemnas},
RegionName.ArmoredXemnas: (RegionName.ArmoredXemnas2,), RegionName.ArmoredXemnas: {RegionName.ArmoredXemnas2},
RegionName.ArmoredXemnas2: (RegionName.FinalXemnas,), RegionName.ArmoredXemnas2: {RegionName.FinalXemnas},
RegionName.LevelsVS1: (RegionName.LevelsVS3,), RegionName.LevelsVS1: {RegionName.LevelsVS3},
RegionName.LevelsVS3: (RegionName.LevelsVS6,), RegionName.LevelsVS3: {RegionName.LevelsVS6},
RegionName.LevelsVS6: (RegionName.LevelsVS9,), RegionName.LevelsVS6: {RegionName.LevelsVS9},
RegionName.LevelsVS9: (RegionName.LevelsVS12,), RegionName.LevelsVS9: {RegionName.LevelsVS12},
RegionName.LevelsVS12: (RegionName.LevelsVS15,), RegionName.LevelsVS12: {RegionName.LevelsVS15},
RegionName.LevelsVS15: (RegionName.LevelsVS18,), RegionName.LevelsVS15: {RegionName.LevelsVS18},
RegionName.LevelsVS18: (RegionName.LevelsVS21,), RegionName.LevelsVS18: {RegionName.LevelsVS21},
RegionName.LevelsVS21: (RegionName.LevelsVS24,), RegionName.LevelsVS21: {RegionName.LevelsVS24},
RegionName.LevelsVS24: (RegionName.LevelsVS26,), RegionName.LevelsVS24: {RegionName.LevelsVS26},
RegionName.AtlanticaSongOne: (RegionName.AtlanticaSongTwo,), RegionName.AtlanticaSongOne: {RegionName.AtlanticaSongTwo},
RegionName.AtlanticaSongTwo: (RegionName.AtlanticaSongThree,), RegionName.AtlanticaSongTwo: {RegionName.AtlanticaSongThree},
RegionName.AtlanticaSongThree: (RegionName.AtlanticaSongFour,), RegionName.AtlanticaSongThree: {RegionName.AtlanticaSongFour},
} }
for source, target in KH2RegionConnections.items(): for source, target in KH2RegionConnections.items():

View File

@@ -194,8 +194,8 @@ class KH2WorldRules(KH2Rules):
RegionName.Oc: lambda state: self.oc_unlocked(state, 1), RegionName.Oc: lambda state: self.oc_unlocked(state, 1),
RegionName.Oc2: lambda state: self.oc_unlocked(state, 2), RegionName.Oc2: lambda state: self.oc_unlocked(state, 2),
#twtnw1 is actually the roxas fight region thus roxas requires 1 way to the dawn
RegionName.Twtnw2: lambda state: self.twtnw_unlocked(state, 2), RegionName.Twtnw2: lambda state: self.twtnw_unlocked(state, 2),
# These will be swapped and First Visit lock for twtnw is in development.
# RegionName.Twtnw1: lambda state: self.lod_unlocked(state, 2), # RegionName.Twtnw1: lambda state: self.lod_unlocked(state, 2),
RegionName.Ht: lambda state: self.ht_unlocked(state, 1), RegionName.Ht: lambda state: self.ht_unlocked(state, 1),
@@ -263,10 +263,7 @@ class KH2WorldRules(KH2Rules):
weapon_region = self.multiworld.get_region(RegionName.Keyblade, self.player) weapon_region = self.multiworld.get_region(RegionName.Keyblade, self.player)
for location in weapon_region.locations: for location in weapon_region.locations:
if location.name in exclusion_table["WeaponSlots"]: # shop items and starting items are not in this list add_rule(location, lambda state: state.has(exclusion_table["WeaponSlots"][location.name], self.player))
exclusion_item = exclusion_table["WeaponSlots"][location.name]
add_rule(location, lambda state, e_item=exclusion_item: state.has(e_item, self.player))
if location.name in Goofy_Checks: if location.name in Goofy_Checks:
add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys()) add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys())
elif location.name in Donald_Checks: elif location.name in Donald_Checks:
@@ -922,8 +919,8 @@ class KH2FightRules(KH2Rules):
# normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus
# hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus
sephiroth_rules = { sephiroth_rules = {
"easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state), "easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1,
"normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([gap_closer], state) >= 1, "normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2,
"hard": self.kh2_dict_count(hard_sephiroth_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2, "hard": self.kh2_dict_count(hard_sephiroth_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2,
} }
return sephiroth_rules[self.fight_logic] return sephiroth_rules[self.fight_logic]

View File

@@ -3,7 +3,7 @@ from typing import List
from BaseClasses import Tutorial, ItemClassification from BaseClasses import Tutorial, ItemClassification
from Fill import fast_fill from Fill import fast_fill
from worlds.LauncherComponents import Component, components, Type, launch as launch_component from worlds.LauncherComponents import Component, components, Type, launch_subprocess
from worlds.AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
from .Items import * from .Items import *
from .Locations import * from .Locations import *
@@ -17,7 +17,7 @@ from .Subclasses import KH2Item
def launch_client(): def launch_client():
from .Client import launch from .Client import launch
launch_component(launch, name="KH2Client") launch_subprocess(launch, name="KH2Client")
components.append(Component("KH2 Client", "KH2Client", func=launch_client, component_type=Type.CLIENT)) components.append(Component("KH2 Client", "KH2Client", func=launch_client, component_type=Type.CLIENT))

View File

@@ -10,7 +10,7 @@
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/) Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) - Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
1. Version 25.01.26.0 or greater OpenKH Mod Manager with Panacea 1. Version 3.4.0 or greater OpenKH Mod Manager with Panacea
2. Lua Backend from the OpenKH Mod Manager 2. Lua Backend from the OpenKH Mod Manager
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager 3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
- Needed for Archipelago - Needed for Archipelago
@@ -52,7 +52,7 @@ After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot
<h2 style="text-transform:none";>What the Mod Manager Should Look Like.</h2> <h2 style="text-transform:none";>What the Mod Manager Should Look Like.</h2>
![image](https://i.imgur.com/N0WJ8Qn.png) ![image](https://i.imgur.com/Si4oZ8w.png)
<h2 style="text-transform:none";>Using the KH2 Client</h2> <h2 style="text-transform:none";>Using the KH2 Client</h2>

View File

@@ -103,7 +103,6 @@ def generateRom(args, world: "LinksAwakeningWorld"):
assembler.const("wGoldenLeaves", 0xDB42) # New memory location where to store the golden leaf counter assembler.const("wGoldenLeaves", 0xDB42) # New memory location where to store the golden leaf counter
assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available (and boots) assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available (and boots)
assembler.const("wCustomMessage", 0xC0A0) assembler.const("wCustomMessage", 0xC0A0)
assembler.const("wOverworldRoomStatus", 0xD800)
# We store the link info in unused color dungeon flags, so it gets preserved in the savegame. # We store the link info in unused color dungeon flags, so it gets preserved in the savegame.
assembler.const("wLinkSyncSequenceNumber", 0xDDF6) assembler.const("wLinkSyncSequenceNumber", 0xDDF6)

View File

@@ -68,12 +68,10 @@ DEFAULT_ITEM_POOL = {
class ItemPool: class ItemPool:
def __init__(self, logic, settings, rnd, stabilize_item_pool: bool): def __init__(self, logic, settings, rnd):
self.__pool = {} self.__pool = {}
self.__setup(logic, settings) self.__setup(logic, settings)
self.__randomizeRupees(settings, rnd)
if not stabilize_item_pool:
self.__randomizeRupees(settings, rnd)
def add(self, item, count=1): def add(self, item, count=1):
self.__pool[item] = self.__pool.get(item, 0) + count self.__pool[item] = self.__pool.get(item, 0) + count

View File

@@ -2,10 +2,6 @@ import typing
from ..checkMetadata import checkMetadataTable from ..checkMetadata import checkMetadataTable
from .constants import * from .constants import *
custom_name_replacements = {
'"':"'",
'_':' ',
}
class ItemInfo: class ItemInfo:
MULTIWORLD = True MULTIWORLD = True
@@ -27,11 +23,6 @@ class ItemInfo:
def setLocation(self, location): def setLocation(self, location):
self._location = location self._location = location
def setCustomItemName(self, name):
for key, val in custom_name_replacements.items():
name = name.replace(key, val)
self.custom_item_name = name
def getOptions(self): def getOptions(self):
return self.OPTIONS return self.OPTIONS

View File

@@ -253,7 +253,7 @@ def isConsumable(item) -> bool:
class RequirementsSettings: class RequirementsSettings:
def __init__(self, options): def __init__(self, options):
self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG, BOMB) self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG)
self.pit_bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB) # unique self.pit_bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB) # unique
self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG) self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG)
self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # hinox, shrouded stalfos self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # hinox, shrouded stalfos

Some files were not shown because too many files have changed in this diff Show More