Skip to content

Commit d7ecc83

Browse files
authoredNov 9, 2024··
Merge pull request #15 from ArchipelagoMW/main
LOCAL add AP changes to main
2 parents 7061e8c + fa93bc5 commit d7ecc83

File tree

232 files changed

+2977
-1950
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

232 files changed

+2977
-1950
lines changed
 

‎BaseClasses.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,9 @@ def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset(
194194
self.player_types[new_id] = NetUtils.SlotType.group
195195
world_type = AutoWorld.AutoWorldRegister.world_types[game]
196196
self.worlds[new_id] = world_type.create_group(self, new_id, players)
197-
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
197+
self.worlds[new_id].collect_item = AutoWorld.World.collect_item.__get__(self.worlds[new_id])
198+
self.worlds[new_id].collect = AutoWorld.World.collect.__get__(self.worlds[new_id])
199+
self.worlds[new_id].remove = AutoWorld.World.remove.__get__(self.worlds[new_id])
198200
self.player_name[new_id] = name
199201

200202
new_group = self.groups[new_id] = Group(name=name, game=game, players=players,
@@ -339,7 +341,7 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
339341
new_item.classification |= classifications[item_name]
340342
new_itempool.append(new_item)
341343

342-
region = Region("Menu", group_id, self, "ItemLink")
344+
region = Region(group["world"].origin_region_name, group_id, self, "ItemLink")
343345
self.regions.append(region)
344346
locations = region.locations
345347
# ensure that progression items are linked first, then non-progression
@@ -720,7 +722,7 @@ def _update_reachable_regions_explicit_indirect_conditions(self, player: int, qu
720722
if new_region in reachable_regions:
721723
blocked_connections.remove(connection)
722724
elif connection.can_reach(self):
723-
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
725+
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
724726
reachable_regions.add(new_region)
725727
blocked_connections.remove(connection)
726728
blocked_connections.update(new_region.exits)
@@ -946,6 +948,7 @@ def __init__(self, player: int, name: str = "", parent: Optional[Region] = None)
946948
self.player = player
947949

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

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

11721175
def place_locked_item(self, item: Item):
@@ -1261,6 +1264,10 @@ def useful(self) -> bool:
12611264
def trap(self) -> bool:
12621265
return ItemClassification.trap in self.classification
12631266

1267+
@property
1268+
def excludable(self) -> bool:
1269+
return not (self.advancement or self.useful)
1270+
12641271
@property
12651272
def flags(self) -> int:
12661273
return self.classification.as_flag()

‎CommonClient.py

+51-17
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,21 @@ def get_ssl_context():
4545

4646

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

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

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

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

171183

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

344356
self.item_names = self.NameLookupDict(self, "item")
345357
self.location_names = self.NameLookupDict(self, "location")
358+
self.versions = {}
359+
self.checksums = {}
346360

347361
self.jsontotextparser = JSONtoTextParser(self)
348362
self.rawjsontotextparser = RawJSONtoTextParser(self)
@@ -429,7 +443,10 @@ async def get_username(self):
429443
self.auth = await self.console_input()
430444

431445
async def send_connect(self, **kwargs: typing.Any) -> None:
432-
""" send `Connect` packet to log in to server """
446+
"""
447+
Send a `Connect` packet to log in to the server,
448+
additional keyword args can override any value in the connection packet
449+
"""
433450
payload = {
434451
'cmd': 'Connect',
435452
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
@@ -439,6 +456,7 @@ async def send_connect(self, **kwargs: typing.Any) -> None:
439456
if kwargs:
440457
payload.update(kwargs)
441458
await self.send_msgs([payload])
459+
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])
442460

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

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

468487
def is_echoed_chat(self, print_json_packet: dict) -> bool:
488+
"""Helper function for filtering out messages sent by self."""
469489
return print_json_packet.get("type", "") == "Chat" \
470490
and print_json_packet.get("team", None) == self.team \
471491
and print_json_packet.get("slot", None) == self.slot
@@ -497,13 +517,14 @@ def on_user_say(self, text: str) -> typing.Optional[str]:
497517
"""Gets called before sending a Say to the server from the user.
498518
Returned text is sent, or sending is aborted if None is returned."""
499519
return text
500-
520+
501521
def on_ui_command(self, text: str) -> None:
502522
"""Gets called by kivy when the user executes a command starting with `/` or `!`.
503523
The command processor is still called; this is just intended for command echoing."""
504524
self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])
505525

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

555-
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
556-
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
557-
# no action required if local version is new enough
558-
if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \
559-
or remote_checksum != local_checksum:
560-
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
561-
cache_version: int = cached_game.get("version", 0)
562-
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
563-
# download remote version if cache is not new enough
564-
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
565-
or remote_checksum != cache_checksum:
566-
needed_updates.add(game)
576+
cached_version: int = self.versions.get(game, 0)
577+
cached_checksum: typing.Optional[str] = self.checksums.get(game)
578+
# no action required if cached version is new enough
579+
if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \
580+
or remote_checksum != cached_checksum:
581+
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
582+
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
583+
if ((remote_checksum or remote_version <= local_version and remote_version != 0)
584+
and remote_checksum == local_checksum):
585+
self.update_game(network_data_package["games"][game], game)
567586
else:
568-
self.update_game(cached_game, game)
587+
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
588+
cache_version: int = cached_game.get("version", 0)
589+
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
590+
# download remote version if cache is not new enough
591+
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
592+
or remote_checksum != cache_checksum:
593+
needed_updates.add(game)
594+
else:
595+
self.update_game(cached_game, game)
569596
if needed_updates:
570597
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
571598

572599
def update_game(self, game_package: dict, game: str):
573600
self.item_names.update_game(game, game_package["item_name_to_id"])
574601
self.location_names.update_game(game, game_package["location_name_to_id"])
602+
self.versions[game] = game_package.get("version", 0)
603+
self.checksums[game] = game_package.get("checksum")
575604

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

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

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

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

9881019

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

1072+
# handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
10401073
if args.url:
10411074
url = urllib.parse.urlparse(args.url)
10421075
if url.scheme == "archipelago":
@@ -1048,6 +1081,7 @@ async def main(args):
10481081
else:
10491082
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
10501083

1084+
# use colorama to display colored text highlighting on windows
10511085
colorama.init()
10521086

10531087
asyncio.run(main(args))

0 commit comments

Comments
 (0)