From 431496aa1bec0844c8f3a15b7675eeeeb05e11cc Mon Sep 17 00:00:00 2001 From: Ehseezed Date: Sun, 28 Jan 2024 12:45:19 -0600 Subject: [PATCH] Add missing locations and IDs Add new root AM2RClient.py Moved old AM2RClient.py to world root Update __init__.py --- AM2RClient.py | 205 ++------------------------------------ worlds/am2r/AM2RClient.py | 200 +++++++++++++++++++++++++++++++++++++ worlds/am2r/__init__.py | 10 ++ worlds/am2r/locations.py | 1 + 4 files changed, 219 insertions(+), 197 deletions(-) create mode 100644 worlds/am2r/AM2RClient.py diff --git a/AM2RClient.py b/AM2RClient.py index ae2b985077d9..0af370788f8a 100644 --- a/AM2RClient.py +++ b/AM2RClient.py @@ -1,200 +1,11 @@ -import asyncio -import copy -import json -import time -from asyncio import StreamReader, StreamWriter -from typing import List -from worlds.AM2R.items import item_table -from worlds.AM2R.locations import get_location_datas +from __future__ import annotations -import Utils -from Utils import async_start -from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ - get_base_parser - -CONNECTION_TIMING_OUT_STATUS = "Connection timing out" -CONNECTION_REFUSED_STATUS = "Connection Refused" -CONNECTION_RESET_STATUS = "Connection was reset" -CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" -CONNECTION_CONNECTED_STATUS = "Connected" -CONNECTION_INITIAL_STATUS = "Connection has not been initiated" -item_location_scouts = {} -item_id_to_game_id: dict = {item.code: item.game_id for item in item_table.values()} -location_id_to_game_id: dict = {location.code: location.game_id for location in get_location_datas(None, None)} -game_id_to_location_id: dict = {location.game_id: location.code for location in get_location_datas(None, None) if location.code != None} - - - -class AM2RCommandProcessor(ClientCommandProcessor): - def __init__(self, ctx: CommonContext): - super().__init__(ctx) - - def _cmd_am2r(self): - """Check AM2R Connection State""" - if isinstance(self.ctx, AM2RContext): - logger.info(f"Connection Status: {self.ctx.am2r_status}") - -class AM2RContext(CommonContext): - command_processor = AM2RCommandProcessor - game = 'AM2R' - items_handling = 0b111 # full remote - - def __init__(self, server_address, password): - super().__init__(server_address, password) - self.waiting_for_client = False - self.am2r_streams: (StreamReader, StreamWriter) = None - self.am2r_sync_task = None - self.am2r_status = CONNECTION_INITIAL_STATUS - self.received_locscouts = False - self.metroids_required = 41 - self.client_requesting_scouts = False - - async def server_auth(self, password_requested: bool = False): - if password_requested and not self.password: - await super().server_auth(password_requested) - if not self.auth: - self.waiting_for_client = True - logger.info('No AM2R details found. Reconnect to MW server after AM2R is connected.') - return - - await self.send_connect() - - def run_gui(self): - from kvui import GameManager - - class AM2RManager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] - base_title = "AM2R Multiworld Client" - - self.ui = AM2RManager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") +import ModuleUpdate +ModuleUpdate.update() - def on_package(self, cmd: str, args: dict): - if cmd == "Connected": - self.metroids_required = args["slot_data"]["MetroidsRequired"] - elif cmd == "LocationInfo": - logger.info("Received Location Info") -def get_payload(ctx: AM2RContext): - items_to_give = [item_id_to_game_id[item.item] for item in ctx.items_received] - if not ctx.locations_info: - locations = [location.code for location in get_location_datas(None, None) if location.code is not None] - async_start(ctx.send_msgs([{"cmd": "LocationScouts", "locations": locations, "create_as_hint": 0}])) - return json.dumps({ - "cmd": "items", "items": items_to_give - }) - - if ctx.client_requesting_scouts: - itemdict = {} - for locationid, netitem in ctx.locations_info.items(): - gamelocation = location_id_to_game_id[locationid] - if netitem.item in item_id_to_game_id: - gameitem = item_id_to_game_id[netitem.item] - else: - gameitem = 20 - itemdict[gamelocation] = gameitem - print("Sending") - return json.dumps({ - 'cmd':"locations", 'items': itemdict, 'metroids': ctx.metroids_required - }) - return json.dumps({ - "cmd": "items", "items": items_to_give - }) - -def parse_payload(ctx: AM2RContext, data_decoded): - item_list = [game_id_to_location_id[int(location)] for location in data_decoded["Items"]] - item_set = set(item_list) - ctx.locations_checked = item_list - new_locations = [location for location in ctx.missing_locations if location in item_set] - if new_locations: - async_start(ctx.send_msgs([{"cmd": "LocationChecks", "locations": new_locations}])) - - -async def am2r_sync_task(ctx: AM2RContext): - logger.info("Starting AM2R connector, use /am2r for status information.") - while not ctx.exit_event.is_set(): - error_status = None - if ctx.am2r_streams: - (reader, writer) = ctx.am2r_streams - msg = get_payload(ctx).encode() - writer.write(msg) - writer.write(b'\n') - try: - await asyncio.wait_for(writer.drain(), timeout=1.5) - try: - data = await asyncio.wait_for(reader.readline(), timeout=5) - data_decoded = json.loads(data.decode()) - ctx.auth = data_decoded["SlotName"] - ctx.client_requesting_scouts = not bool(int(data_decoded["SeedReceived"])) - parse_payload(ctx, data_decoded) - except asyncio.TimeoutError: - logger.debug("Read Timed Out, Reconnecting") - error_status = CONNECTION_TIMING_OUT_STATUS - writer.close() - ctx.am2r_streams = None - except ConnectionResetError as e: - logger.debug("Read failed due to Connection Lost, Reconnecting") - error_status = CONNECTION_RESET_STATUS - writer.close() - ctx.am2r_streams = None - except TimeoutError: - logger.debug("Connection Timed Out, Reconnecting") - error_status = CONNECTION_TIMING_OUT_STATUS - writer.close() - ctx.am2r_streams = None - except ConnectionResetError: - logger.debug("Connection Lost, Reconnecting") - error_status = CONNECTION_RESET_STATUS - writer.close() - ctx.am2r_streams = None - - if ctx.am2r_status == CONNECTION_TENTATIVE_STATUS: - if not error_status: - logger.info("Successfully Connected to AM2R") - ctx.am2r_status = CONNECTION_CONNECTED_STATUS - else: - ctx.am2r_status = f"Was tentatively connected but error occured: {error_status}" - elif error_status: - ctx.am2r_status = error_status - logger.info("Lost connection to AM2R and attempting to reconnect. Use /am2r for status updates") - else: - try: - logger.debug("Attempting to connect to AM2R") - ctx.am2r_streams = await asyncio.wait_for(asyncio.open_connection("127.0.0.1", 64197), timeout=10) - ctx.am2r_status = CONNECTION_TENTATIVE_STATUS - except TimeoutError: - logger.debug("Connection Timed Out, Trying Again") - ctx.am2r_status = CONNECTION_TIMING_OUT_STATUS - continue - except ConnectionRefusedError: - logger.debug("Connection Refused, Trying Again") - ctx.am2r_status = CONNECTION_REFUSED_STATUS - continue - - -if __name__ == '__main__': - # Text Mode to use !hint and such with games that have no text entry - Utils.init_logging("AM2RClient") - - options = Utils.get_options() - - async def main(args): - ctx = AM2RContext(args.connect, args.password) - ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") - if gui_enabled: - ctx.run_gui() - ctx.run_cli() - ctx.am2r_sync_task = asyncio.create_task(am2r_sync_task(ctx), name="AM2R Sync") - await ctx.exit_event.wait() - ctx.server_address = None - - await ctx.shutdown() - - import colorama +from worlds.am2r.AM2RClient import launch +import Utils - parser = get_base_parser() - args = parser.parse_args() - colorama.init() - asyncio.run(main(args)) - colorama.deinit() \ No newline at end of file +if __name__ == "__main__": + Utils.init_logging("AM2RClient", exception_logger="Client") + launch() diff --git a/worlds/am2r/AM2RClient.py b/worlds/am2r/AM2RClient.py new file mode 100644 index 000000000000..703d2b508813 --- /dev/null +++ b/worlds/am2r/AM2RClient.py @@ -0,0 +1,200 @@ +import asyncio +import copy +import json +import time +from asyncio import StreamReader, StreamWriter +from typing import List +from worlds.am2r.items import item_table +from worlds.am2r.locations import get_location_datas + +import Utils +from Utils import async_start +from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ + get_base_parser + +CONNECTION_TIMING_OUT_STATUS = "Connection timing out" +CONNECTION_REFUSED_STATUS = "Connection Refused" +CONNECTION_RESET_STATUS = "Connection was reset" +CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" +CONNECTION_CONNECTED_STATUS = "Connected" +CONNECTION_INITIAL_STATUS = "Connection has not been initiated" +item_location_scouts = {} +item_id_to_game_id: dict = {item.code: item.game_id for item in item_table.values()} +location_id_to_game_id: dict = {location.code: location.game_id for location in get_location_datas(None, None)} +game_id_to_location_id: dict = {location.game_id: location.code for location in get_location_datas(None, None) if location.code != None} + + + +class AM2RCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx: CommonContext): + super().__init__(ctx) + + def _cmd_am2r(self): + """Check AM2R Connection State""" + if isinstance(self.ctx, AM2RContext): + logger.info(f"Connection Status: {self.ctx.am2r_status}") + +class AM2RContext(CommonContext): + command_processor = AM2RCommandProcessor + game = 'AM2R' + items_handling = 0b111 # full remote + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.waiting_for_client = False + self.am2r_streams: (StreamReader, StreamWriter) = None + self.am2r_sync_task = None + self.am2r_status = CONNECTION_INITIAL_STATUS + self.received_locscouts = False + self.metroids_required = 41 + self.client_requesting_scouts = False + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super().server_auth(password_requested) + if not self.auth: + self.waiting_for_client = True + logger.info('No AM2R details found. Reconnect to MW server after AM2R is connected.') + return + + await self.send_connect() + + def run_gui(self): + from kvui import GameManager + + class AM2RManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "AM2R Multiworld Client" + + self.ui = AM2RManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + def on_package(self, cmd: str, args: dict): + if cmd == "Connected": + self.metroids_required = args["slot_data"]["MetroidsRequired"] + elif cmd == "LocationInfo": + logger.info("Received Location Info") +def get_payload(ctx: AM2RContext): + items_to_give = [item_id_to_game_id[item.item] for item in ctx.items_received] + if not ctx.locations_info: + locations = [location.code for location in get_location_datas(None, None) if location.code is not None] + async_start(ctx.send_msgs([{"cmd": "LocationScouts", "locations": locations, "create_as_hint": 0}])) + return json.dumps({ + "cmd": "items", "items": items_to_give + }) + + if ctx.client_requesting_scouts: + itemdict = {} + for locationid, netitem in ctx.locations_info.items(): + gamelocation = location_id_to_game_id[locationid] + if netitem.item in item_id_to_game_id: + gameitem = item_id_to_game_id[netitem.item] + else: + gameitem = 20 + itemdict[gamelocation] = gameitem + print("Sending") + return json.dumps({ + 'cmd':"locations", 'items': itemdict, 'metroids': ctx.metroids_required + }) + return json.dumps({ + "cmd": "items", "items": items_to_give + }) + +def parse_payload(ctx: AM2RContext, data_decoded): + item_list = [game_id_to_location_id[int(location)] for location in data_decoded["Items"]] + item_set = set(item_list) + ctx.locations_checked = item_list + new_locations = [location for location in ctx.missing_locations if location in item_set] + if new_locations: + async_start(ctx.send_msgs([{"cmd": "LocationChecks", "locations": new_locations}])) + + +async def am2r_sync_task(ctx: AM2RContext): + logger.info("Starting AM2R connector, use /am2r for status information.") + while not ctx.exit_event.is_set(): + error_status = None + if ctx.am2r_streams: + (reader, writer) = ctx.am2r_streams + msg = get_payload(ctx).encode() + writer.write(msg) + writer.write(b'\n') + try: + await asyncio.wait_for(writer.drain(), timeout=1.5) + try: + data = await asyncio.wait_for(reader.readline(), timeout=5) + data_decoded = json.loads(data.decode()) + ctx.auth = data_decoded["SlotName"] + ctx.client_requesting_scouts = not bool(int(data_decoded["SeedReceived"])) + parse_payload(ctx, data_decoded) + except asyncio.TimeoutError: + logger.debug("Read Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.am2r_streams = None + except ConnectionResetError as e: + logger.debug("Read failed due to Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.am2r_streams = None + except TimeoutError: + logger.debug("Connection Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.am2r_streams = None + except ConnectionResetError: + logger.debug("Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.am2r_streams = None + + if ctx.am2r_status == CONNECTION_TENTATIVE_STATUS: + if not error_status: + logger.info("Successfully Connected to AM2R") + ctx.am2r_status = CONNECTION_CONNECTED_STATUS + else: + ctx.am2r_status = f"Was tentatively connected but error occured: {error_status}" + elif error_status: + ctx.am2r_status = error_status + logger.info("Lost connection to AM2R and attempting to reconnect. Use /am2r for status updates") + else: + try: + logger.debug("Attempting to connect to AM2R") + ctx.am2r_streams = await asyncio.wait_for(asyncio.open_connection("127.0.0.1", 64197), timeout=10) + ctx.am2r_status = CONNECTION_TENTATIVE_STATUS + except TimeoutError: + logger.debug("Connection Timed Out, Trying Again") + ctx.am2r_status = CONNECTION_TIMING_OUT_STATUS + continue + except ConnectionRefusedError: + logger.debug("Connection Refused, Trying Again") + ctx.am2r_status = CONNECTION_REFUSED_STATUS + continue + + +def launch(): + # Text Mode to use !hint and such with games that have no text entry + Utils.init_logging("AM2RClient") + + options = Utils.get_options() + + async def main(args): + ctx = AM2RContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + ctx.am2r_sync_task = asyncio.create_task(am2r_sync_task(ctx), name="AM2R Sync") + await ctx.exit_event.wait() + ctx.server_address = None + + await ctx.shutdown() + + import colorama + + parser = get_base_parser() + args = parser.parse_args() + colorama.init() + asyncio.run(main(args)) + colorama.deinit() \ No newline at end of file diff --git a/worlds/am2r/__init__.py b/worlds/am2r/__init__.py index 489cb5d426ca..24e6d78c3d6f 100644 --- a/worlds/am2r/__init__.py +++ b/worlds/am2r/__init__.py @@ -1,3 +1,4 @@ +import types from typing import Dict from .items import item_table from .locations import get_location_datas, EventId @@ -5,6 +6,15 @@ from BaseClasses import Tutorial, Item from .options import AM2R_options, MetroidsAreChecks from worlds.AutoWorld import World, WebWorld +from worlds.LauncherComponents import Component, components + + +def launch_client(): + from AM2RClient import launch + launch() + + +components.append(Component("AM2R Client", "AM2RClient")) class AM2RWeb(WebWorld): diff --git a/worlds/am2r/locations.py b/worlds/am2r/locations.py index 0281b9364dc1..f453febfd732 100644 --- a/worlds/am2r/locations.py +++ b/worlds/am2r/locations.py @@ -73,6 +73,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int]): LocationData("Pre Industrial Complex", "Industrial Complex: In the Sand", 8680043, 211), LocationData("Pre Industrial Complex", "Industrial Complex: Complex Side After Tunnel", 8680044, 202, lambda state: (state.has("Speed Booster", player) or logic.AM2R_can_spider(state)) and logic.AM2R_can_bomb(state)), LocationData("Pre Industrial Complex", "Industrial Complex: Complex Side Tunnel", 8680045, 200, lambda state: state.has("Speed Booster", player) or logic.AM2R_can_spider(state)), + LocationData("Pre Industrial Complex", "Industrial Complex: Behind the Green Door", 8680146, 212, lambda state: state.has("Speed Booster", player) or logic.AM2R_can_spider(state)), LocationData("Pre Industrial Complex", "Industrial Complex: Save Room", 8680046, 203, lambda state: state.has("Speed Booster", player) or logic.AM2R_can_spider(state)), LocationData("Pre Industrial Complex", "Industrial Complex: Spazer", 8680047, 13, lambda state: state.has("Speed Booster", player) or logic.AM2R_can_spider(state)), LocationData("Pre Industrial Complex", "Industrial Complex: Gamma Spark", 8680048, 204, lambda state: state.has("Speed Booster", player)),