CommonClient: update commands to function without local apworld (#3045)

This commit is contained in:
qwint
2025-07-25 21:18:36 -05:00
committed by GitHub
parent f66d8e9a61
commit 23f0b720de

View File

@@ -21,7 +21,7 @@ import Utils
if __name__ == "__main__": if __name__ == "__main__":
Utils.init_logging("TextClient", exception_logger="Client") Utils.init_logging("TextClient", exception_logger="Client")
from MultiServer import CommandProcessor from MultiServer import CommandProcessor, mark_raw
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot, from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType) RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType)
from Utils import Version, stream_input, async_start from Utils import Version, stream_input, async_start
@@ -99,6 +99,17 @@ class ClientCommandProcessor(CommandProcessor):
self.ctx.on_print_json({"data": parts, "cmd": "PrintJSON"}) self.ctx.on_print_json({"data": parts, "cmd": "PrintJSON"})
return True return True
def get_current_datapackage(self) -> dict[str, typing.Any]:
"""
Return datapackage for current game if known.
:return: The datapackage for the currently registered game. If not found, an empty dictionary will be returned.
"""
if not self.ctx.game:
return {}
checksum = self.ctx.checksums[self.ctx.game]
return Utils.load_data_package_for_checksum(self.ctx.game, checksum)
def _cmd_missing(self, filter_text = "") -> bool: def _cmd_missing(self, filter_text = "") -> bool:
"""List all missing location checks, from your local game state. """List all missing location checks, from your local game state.
Can be given text, which will be used as filter.""" Can be given text, which will be used as filter."""
@@ -107,7 +118,9 @@ class ClientCommandProcessor(CommandProcessor):
return False return False
count = 0 count = 0
checked_count = 0 checked_count = 0
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
lookup = self.get_current_datapackage().get("location_name_to_id", {})
for location, location_id in lookup.items():
if filter_text and filter_text not in location: if filter_text and filter_text not in location:
continue continue
if location_id < 0: if location_id < 0:
@@ -128,43 +141,91 @@ class ClientCommandProcessor(CommandProcessor):
self.output("No missing location checks found.") self.output("No missing location checks found.")
return True return True
def _cmd_items(self): def output_datapackage_part(self, key: str, name: str) -> bool:
"""
Helper to digest a specific section of this game's datapackage.
:param key: The dictionary key in the datapackage.
:param name: Printed to the user as context for the part.
:return: Whether the process was successful.
"""
if not self.ctx.game:
self.output(f"No game set, cannot determine {name}.")
return False
lookup = self.get_current_datapackage().get(key)
if lookup is None:
self.output("datapackage not yet loaded, try again")
return False
self.output(f"{name} for {self.ctx.game}")
for key in lookup:
self.output(key)
return True
def _cmd_items(self) -> bool:
"""List all item names for the currently running game.""" """List all item names for the currently running game."""
if not self.ctx.game: return self.output_datapackage_part("item_name_to_id", "Item Names")
self.output("No game set, cannot determine existing items.")
return False
self.output(f"Item Names for {self.ctx.game}")
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
self.output(item_name)
def _cmd_item_groups(self): def _cmd_locations(self) -> bool:
"""List all item group names for the currently running game."""
if not self.ctx.game:
self.output("No game set, cannot determine existing item groups.")
return False
self.output(f"Item Group Names for {self.ctx.game}")
for group_name in AutoWorldRegister.world_types[self.ctx.game].item_name_groups:
self.output(group_name)
def _cmd_locations(self):
"""List all location names for the currently running game.""" """List all location names for the currently running game."""
if not self.ctx.game: return self.output_datapackage_part("location_name_to_id", "Location Names")
self.output("No game set, cannot determine existing locations.")
return False
self.output(f"Location Names for {self.ctx.game}")
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
self.output(location_name)
def _cmd_location_groups(self): def output_group_part(self, group_key: typing.Literal["item_name_groups", "location_name_groups"],
"""List all location group names for the currently running game.""" filter_key: str,
if not self.ctx.game: name: str) -> bool:
self.output("No game set, cannot determine existing location groups.") """
return False Logs an item or location group from the player's game's datapackage.
self.output(f"Location Group Names for {self.ctx.game}")
for group_name in AutoWorldRegister.world_types[self.ctx.game].location_name_groups:
self.output(group_name)
def _cmd_ready(self): :param group_key: Either Item or Location group to be processed.
:param filter_key: Which group key to filter to. If an empty string is passed will log all item/location groups.
:param name: Printed to the user as context for the part.
:return: Whether the process was successful.
"""
if not self.ctx.game:
self.output(f"No game set, cannot determine existing {name} Groups.")
return False
lookup = Utils.persistent_load().get("groups_by_checksum", {}).get(self.ctx.checksums[self.ctx.game], {})\
.get(self.ctx.game, {}).get(group_key, {})
if lookup is None:
self.output("datapackage not yet loaded, try again")
return False
if filter_key:
if filter_key not in lookup:
self.output(f"Unknown {name} Group {filter_key}")
return False
self.output(f"{name}s for {name} Group \"{filter_key}\"")
for entry in lookup[filter_key]:
self.output(entry)
else:
self.output(f"{name} Groups for {self.ctx.game}")
for group in lookup:
self.output(group)
return True
@mark_raw
def _cmd_item_groups(self, key: str = "") -> bool:
"""
List all item group names for the currently running game.
:param key: Which item group to filter to. Will log all groups if empty.
"""
return self.output_group_part("item_name_groups", key, "Item")
@mark_raw
def _cmd_location_groups(self, key: str = "") -> bool:
"""
List all location group names for the currently running game.
:param key: Which item group to filter to. Will log all groups if empty.
"""
return self.output_group_part("location_name_groups", key, "Location")
def _cmd_ready(self) -> bool:
"""Send ready status to server.""" """Send ready status to server."""
self.ctx.ready = not self.ctx.ready self.ctx.ready = not self.ctx.ready
if self.ctx.ready: if self.ctx.ready:
@@ -174,6 +235,7 @@ class ClientCommandProcessor(CommandProcessor):
state = ClientStatus.CLIENT_CONNECTED state = ClientStatus.CLIENT_CONNECTED
self.output("Unreadied.") self.output("Unreadied.")
async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate") async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
return True
def default(self, raw: str): def default(self, raw: str):
"""The default message parser to be used when parsing any messages that do not match a command""" """The default message parser to be used when parsing any messages that do not match a command"""
@@ -379,6 +441,8 @@ class CommonContext:
self.jsontotextparser = JSONtoTextParser(self) self.jsontotextparser = JSONtoTextParser(self)
self.rawjsontotextparser = RawJSONtoTextParser(self) self.rawjsontotextparser = RawJSONtoTextParser(self)
if self.game:
self.checksums[self.game] = network_data_package["games"][self.game]["checksum"]
self.update_data_package(network_data_package) self.update_data_package(network_data_package)
# execution # execution
@@ -638,6 +702,24 @@ class CommonContext:
for game, game_data in data_package["games"].items(): for game, game_data in data_package["games"].items():
Utils.store_data_package_for_checksum(game, game_data) Utils.store_data_package_for_checksum(game, game_data)
def consume_network_item_groups(self):
data = {"item_name_groups": self.stored_data[f"_read_item_name_groups_{self.game}"]}
current_cache = Utils.persistent_load().get("groups_by_checksum", {}).get(self.checksums[self.game], {})
if self.game in current_cache:
current_cache[self.game].update(data)
else:
current_cache[self.game] = data
Utils.persistent_store("groups_by_checksum", self.checksums[self.game], current_cache)
def consume_network_location_groups(self):
data = {"location_name_groups": self.stored_data[f"_read_location_name_groups_{self.game}"]}
current_cache = Utils.persistent_load().get("groups_by_checksum", {}).get(self.checksums[self.game], {})
if self.game in current_cache:
current_cache[self.game].update(data)
else:
current_cache[self.game] = data
Utils.persistent_store("groups_by_checksum", self.checksums[self.game], current_cache)
# data storage # data storage
def set_notify(self, *keys: str) -> None: def set_notify(self, *keys: str) -> None:
@@ -938,6 +1020,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.hint_points = args.get("hint_points", 0) ctx.hint_points = args.get("hint_points", 0)
ctx.consume_players_package(args["players"]) ctx.consume_players_package(args["players"])
ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}") ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}")
if ctx.game:
game = ctx.game
else:
game = ctx.slot_info[ctx.slot][1]
ctx.stored_data_notification_keys.add(f"_read_item_name_groups_{game}")
ctx.stored_data_notification_keys.add(f"_read_location_name_groups_{game}")
msgs = [] msgs = []
if ctx.locations_checked: if ctx.locations_checked:
msgs.append({"cmd": "LocationChecks", msgs.append({"cmd": "LocationChecks",
@@ -1018,11 +1106,19 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.stored_data.update(args["keys"]) ctx.stored_data.update(args["keys"])
if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" in args["keys"]: if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" in args["keys"]:
ctx.ui.update_hints() ctx.ui.update_hints()
if f"_read_item_name_groups_{ctx.game}" in args["keys"]:
ctx.consume_network_item_groups()
if f"_read_location_name_groups_{ctx.game}" in args["keys"]:
ctx.consume_network_location_groups()
elif cmd == "SetReply": elif cmd == "SetReply":
ctx.stored_data[args["key"]] = args["value"] ctx.stored_data[args["key"]] = args["value"]
if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" == args["key"]: if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" == args["key"]:
ctx.ui.update_hints() ctx.ui.update_hints()
elif f"_read_item_name_groups_{ctx.game}" == args["key"]:
ctx.consume_network_item_groups()
elif f"_read_location_name_groups_{ctx.game}" == args["key"]:
ctx.consume_network_location_groups()
elif args["key"].startswith("EnergyLink"): elif args["key"].startswith("EnergyLink"):
ctx.current_energy_link_value = args["value"] ctx.current_energy_link_value = args["value"]
if ctx.ui: if ctx.ui: