Skip to content

Commit

Permalink
Merge branch 'main' into ladx/stop-randomly-swapping-rupees-in-pool
Browse files Browse the repository at this point in the history
  • Loading branch information
threeandthreee authored Nov 1, 2024
2 parents e83a64f + 1323474 commit d4db618
Show file tree
Hide file tree
Showing 281 changed files with 5,748 additions and 4,111 deletions.
13 changes: 9 additions & 4 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset(
self.player_types[new_id] = NetUtils.SlotType.group
world_type = AutoWorld.AutoWorldRegister.world_types[game]
self.worlds[new_id] = world_type.create_group(self, new_id, players)
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
self.worlds[new_id].collect_item = AutoWorld.World.collect_item.__get__(self.worlds[new_id])
self.worlds[new_id].collect = AutoWorld.World.collect.__get__(self.worlds[new_id])
self.worlds[new_id].remove = AutoWorld.World.remove.__get__(self.worlds[new_id])
self.player_name[new_id] = name

new_group = self.groups[new_id] = Group(name=name, game=game, players=players,
Expand Down Expand Up @@ -339,9 +341,11 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
new_item.classification |= classifications[item_name]
new_itempool.append(new_item)

region = Region("Menu", group_id, self, "ItemLink")
region = Region(group["world"].origin_region_name, group_id, self, "ItemLink")
self.regions.append(region)
locations = region.locations
# ensure that progression items are linked first, then non-progression
self.itempool.sort(key=lambda item: item.advancement)
for item in self.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0)
if count:
Expand Down Expand Up @@ -718,7 +722,7 @@ def _update_reachable_regions_explicit_indirect_conditions(self, player: int, qu
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
Expand Down Expand Up @@ -944,6 +948,7 @@ def __init__(self, player: int, name: str = "", parent: Optional[Region] = None)
self.player = player

def can_reach(self, state: CollectionState) -> bool:
assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region"
if self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and not self in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
Expand Down Expand Up @@ -1164,7 +1169,7 @@ def can_fill(self, state: CollectionState, item: Item, check_access: bool = True

def can_reach(self, state: CollectionState) -> bool:
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
assert self.parent_region, "Can't reach location without region"
assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region"
return self.parent_region.can_reach(state) and self.access_rule(state)

def place_locked_item(self, item: Item):
Expand Down
3 changes: 2 additions & 1 deletion BizHawkClient.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations

import sys
import ModuleUpdate
ModuleUpdate.update()

from worlds._bizhawk.context import launch

if __name__ == "__main__":
launch()
launch(*sys.argv[1:])
68 changes: 51 additions & 17 deletions CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,21 @@ def get_ssl_context():


class ClientCommandProcessor(CommandProcessor):
"""
The Command Processor will parse every method of the class that starts with "_cmd_" as a command to be called
when parsing user input, i.e. _cmd_exit will be called when the user sends the command "/exit".
The decorator @mark_raw can be imported from MultiServer and tells the parser to only split on the first
space after the command i.e. "/exit one two three" will be passed in as method("one two three") with mark_raw
and method("one", "two", "three") without.
In addition all docstrings for command methods will be displayed to the user on launch and when using "/help"
"""
def __init__(self, ctx: CommonContext):
self.ctx = ctx

def output(self, text: str):
"""Helper function to abstract logging to the CommonClient UI"""
logger.info(text)

def _cmd_exit(self) -> bool:
Expand Down Expand Up @@ -164,13 +175,14 @@ def _cmd_ready(self):
async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")

def default(self, raw: str):
"""The default message parser to be used when parsing any messages that do not match a command"""
raw = self.ctx.on_user_say(raw)
if raw:
async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")


class CommonContext:
# Should be adjusted as needed in subclasses
# The following attributes are used to Connect and should be adjusted as needed in subclasses
tags: typing.Set[str] = {"AP"}
game: typing.Optional[str] = None
items_handling: typing.Optional[int] = None
Expand Down Expand Up @@ -343,6 +355,8 @@ def __init__(self, server_address: typing.Optional[str] = None, password: typing

self.item_names = self.NameLookupDict(self, "item")
self.location_names = self.NameLookupDict(self, "location")
self.versions = {}
self.checksums = {}

self.jsontotextparser = JSONtoTextParser(self)
self.rawjsontotextparser = RawJSONtoTextParser(self)
Expand Down Expand Up @@ -429,7 +443,10 @@ async def get_username(self):
self.auth = await self.console_input()

async def send_connect(self, **kwargs: typing.Any) -> None:
""" send `Connect` packet to log in to server """
"""
Send a `Connect` packet to log in to the server,
additional keyword args can override any value in the connection packet
"""
payload = {
'cmd': 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
Expand All @@ -439,6 +456,7 @@ async def send_connect(self, **kwargs: typing.Any) -> None:
if kwargs:
payload.update(kwargs)
await self.send_msgs([payload])
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])

async def console_input(self) -> str:
if self.ui:
Expand All @@ -459,13 +477,15 @@ def cancel_autoreconnect(self) -> bool:
return False

def slot_concerns_self(self, slot) -> bool:
"""Helper function to abstract player groups, should be used instead of checking slot == self.slot directly."""
if slot == self.slot:
return True
if slot in self.slot_info:
return self.slot in self.slot_info[slot].group_members
return False

def is_echoed_chat(self, print_json_packet: dict) -> bool:
"""Helper function for filtering out messages sent by self."""
return print_json_packet.get("type", "") == "Chat" \
and print_json_packet.get("team", None) == self.team \
and print_json_packet.get("slot", None) == self.slot
Expand Down Expand Up @@ -497,13 +517,14 @@ def on_user_say(self, text: str) -> typing.Optional[str]:
"""Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned."""
return text

def on_ui_command(self, text: str) -> None:
"""Gets called by kivy when the user executes a command starting with `/` or `!`.
The command processor is still called; this is just intended for command echoing."""
self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])

def update_permissions(self, permissions: typing.Dict[str, int]):
"""Internal method to parse and save server permissions from RoomInfo"""
for permission_name, permission_flag in permissions.items():
try:
flag = Permission(permission_flag)
Expand Down Expand Up @@ -552,26 +573,34 @@ async def prepare_data_package(self, relevant_games: typing.Set[str],
needed_updates.add(game)
continue

local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
# no action required if local version is new enough
if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \
or remote_checksum != local_checksum:
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
cache_version: int = cached_game.get("version", 0)
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
# download remote version if cache is not new enough
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
or remote_checksum != cache_checksum:
needed_updates.add(game)
cached_version: int = self.versions.get(game, 0)
cached_checksum: typing.Optional[str] = self.checksums.get(game)
# no action required if cached version is new enough
if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \
or remote_checksum != cached_checksum:
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
if ((remote_checksum or remote_version <= local_version and remote_version != 0)
and remote_checksum == local_checksum):
self.update_game(network_data_package["games"][game], game)
else:
self.update_game(cached_game, game)
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
cache_version: int = cached_game.get("version", 0)
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
# download remote version if cache is not new enough
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
or remote_checksum != cache_checksum:
needed_updates.add(game)
else:
self.update_game(cached_game, game)
if needed_updates:
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])

def update_game(self, game_package: dict, game: str):
self.item_names.update_game(game, game_package["item_name_to_id"])
self.location_names.update_game(game, game_package["location_name_to_id"])
self.versions[game] = game_package.get("version", 0)
self.checksums[game] = game_package.get("checksum")

def update_data_package(self, data_package: dict):
for game, game_data in data_package["games"].items():
Expand Down Expand Up @@ -613,6 +642,7 @@ def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
logger.info(f"DeathLink: Received from {data['source']}")

async def send_death(self, death_text: str = ""):
"""Helper function to send a deathlink using death_text as the unique death cause string."""
if self.server and self.server.socket:
logger.info("DeathLink: Sending death to your friends...")
self.last_death_link = time.time()
Expand All @@ -626,6 +656,7 @@ async def send_death(self, death_text: str = ""):
}])

async def update_death_link(self, death_link: bool):
"""Helper function to set Death Link connection tag on/off and update the connection if already connected."""
old_tags = self.tags.copy()
if death_link:
self.tags.add("DeathLink")
Expand All @@ -635,7 +666,7 @@ async def update_death_link(self, death_link: bool):
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])

def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]:
"""Displays an error messagebox"""
"""Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework"""
if not self.ui:
return None
title = title or "Error"
Expand Down Expand Up @@ -987,6 +1018,7 @@ async def console_loop(ctx: CommonContext):


def get_base_parser(description: typing.Optional[str] = None):
"""Base argument parser to be reused for components subclassing off of CommonClient"""
import argparse
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
Expand Down Expand Up @@ -1037,6 +1069,7 @@ async def main(args):
parser.add_argument("url", nargs="?", help="Archipelago connection url")
args = parser.parse_args(args)

# 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":
Expand All @@ -1048,6 +1081,7 @@ async def main(args):
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
colorama.init()

asyncio.run(main(args))
Expand Down
20 changes: 9 additions & 11 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,28 +475,26 @@ def mark_for_locking(location: Location):
nonlocal lock_later
lock_later.append(location)

single_player = multiworld.players == 1 and not multiworld.groups

if prioritylocations:
# "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking,
name="Priority")
single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority")
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations

if progitempool:
# "advancement/progression fill"
if panic_method == "swap":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=True,
name="Progression", single_player_placement=multiworld.players == 1)
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True,
name="Progression", single_player_placement=single_player)
elif panic_method == "raise":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False,
name="Progression", single_player_placement=multiworld.players == 1)
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
name="Progression", single_player_placement=single_player)
elif panic_method == "start_inventory":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False, allow_partial=True,
name="Progression", single_player_placement=multiworld.players == 1)
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
allow_partial=True, name="Progression", single_player_placement=single_player)
if progitempool:
for item in progitempool:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
Expand Down
31 changes: 5 additions & 26 deletions Generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ def mystery_argparse():
parser.add_argument('--race', action='store_true', default=defaults.race)
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
parser.add_argument('--log_level', default='info', help='Sets log level')
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default=defaults.plando_options,
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
parser.add_argument("--csv_output", action="store_true",
help="Output rolled player options to csv (made for async multiworld).")
parser.add_argument("--plando", default=defaults.plando_options,
help="List of options that can be set manually. Can be combined, for example \"bosses, items\"")
parser.add_argument("--skip_prog_balancing", action="store_true",
help="Skip progression balancing step during generation.")
parser.add_argument("--skip_output", action="store_true",
Expand Down Expand Up @@ -156,6 +156,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
erargs.skip_prog_balancing = args.skip_prog_balancing
erargs.skip_output = args.skip_output
erargs.name = {}
erargs.csv_output = args.csv_output

settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
Expand Down Expand Up @@ -216,28 +217,6 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}")

if args.yaml_output:
import yaml
important = {}
for option, player_settings in vars(erargs).items():
if type(player_settings) == dict:
if all(type(value) != list for value in player_settings.values()):
if len(player_settings.values()) > 1:
important[option] = {player: value for player, value in player_settings.items() if
player <= args.yaml_output}
else:
logging.debug(f"No player settings defined for option '{option}'")

else:
if player_settings != "": # is not empty name
important[option] = player_settings
else:
logging.debug(f"No player settings defined for option '{option}'")
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
yaml.dump(important, f)

return erargs, seed


Expand Down
6 changes: 4 additions & 2 deletions Launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@


def open_host_yaml():
file = settings.get_settings().filename
s = settings.get_settings()
file = s.filename
s.save()
assert file, "host.yaml missing"
if is_linux:
exe = which('sensible-editor') or which('gedit') or \
Expand Down Expand Up @@ -252,7 +254,7 @@ class Launcher(App):
_client_layout: Optional[ScrollBox] = None

def __init__(self, ctx=None):
self.title = self.base_title
self.title = self.base_title + " " + Utils.__version__
self.ctx = ctx
self.icon = r"data/icon.png"
super().__init__()
Expand Down
Loading

0 comments on commit d4db618

Please sign in to comment.