Skip to content

Commit

Permalink
Merge pull request #8 from DodoBirby/main
Browse files Browse the repository at this point in the history
AM2RClient
  • Loading branch information
Ehseezed authored Jan 27, 2024
2 parents 823a172 + c43c79b commit 61ae729
Show file tree
Hide file tree
Showing 3 changed files with 391 additions and 187 deletions.
200 changes: 200 additions & 0 deletions AM2RClient.py
Original file line number Diff line number Diff line change
@@ -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


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

parser = get_base_parser()
args = parser.parse_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()
43 changes: 23 additions & 20 deletions worlds/am2r/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ class ItemData(NamedTuple):
code: int
group: str
classification: ItemClassification = ItemClassification.progression
game_id: int = 0
required_num: int = 0


class AM2RItem(Item):
game: str = "AM2R"
Expand Down Expand Up @@ -78,32 +79,32 @@ def create_all_items(multiworld: MultiWorld, player: int) -> None:


item_table: Dict[str, ItemData] = {
"Missile": ItemData(8678000, "Ammo", ItemClassification.filler),
"Super Missile": ItemData(8678001, "Ammo", ItemClassification.progression, 1),
"Power Bomb": ItemData(8678002, "Ammo", ItemClassification.progression, 2),
"Energy Tank": ItemData(8678003, "Ammo", ItemClassification.filler, 1),
"Missile": ItemData(8678000, "Ammo", ItemClassification.filler, 15),
"Super Missile": ItemData(8678001, "Ammo", ItemClassification.progression, 16, 1),
"Power Bomb": ItemData(8678002, "Ammo", ItemClassification.progression, 18, 2),
"Energy Tank": ItemData(8678003, "Ammo", ItemClassification.filler, 17, 1),
# "Arm Cannon": ItemData8678004, ("Equipment", ItemClassification.progression, 1),
# "Morph Ball": ItemData8678005, ("Equipment", ItemClassification.progression, 1),
# "Power Grip": ItemData8678006, ("Equipment", ItemClassification.progression, 1),
"Bombs": ItemData(8678007, "Equipment", ItemClassification.progression, 1),
"Spider Ball": ItemData(8678008, "Equipment", ItemClassification.progression, 1),
"Hi Jump": ItemData(8678009, "Equipment", ItemClassification.progression, 1),
"Spring Ball": ItemData(8678010, "Equipment", ItemClassification.progression, 1),
"Space Jump": ItemData(8678011, "Equipment", ItemClassification.progression, 1),
"Speed Booster": ItemData(8678012, "Equipment", ItemClassification.progression, 1),
"Screw Attack": ItemData(8678013, "Equipment", ItemClassification.progression, 1),
"Varia Suit": ItemData(8678014, "Equipment", ItemClassification.useful, 1),
"Gravity Suit": ItemData(8678015, "Equipment", ItemClassification.progression, 1),
"Charge Beam": ItemData(8678016, "Beam", ItemClassification.useful, 1),
"Wave Beam": ItemData(8678017, "Beam", ItemClassification.useful, 1),
"Spazer": ItemData(8678018, "Beam", ItemClassification.useful, 1),
"Plasma Beam": ItemData(8678019, "Beam", ItemClassification.useful, 1),
"Ice Beam": ItemData(8678020, "Beam", ItemClassification.progression, 1),
"Bombs": ItemData(8678007, "Equipment", ItemClassification.progression, 0, 1),
"Spider Ball": ItemData(8678008, "Equipment", ItemClassification.progression, 2, 1),
"Hi Jump": ItemData(8678009, "Equipment", ItemClassification.progression, 4, 1),
"Spring Ball": ItemData(8678010, "Equipment", ItemClassification.progression, 3, 1),
"Space Jump": ItemData(8678011, "Equipment", ItemClassification.progression, 6, 1),
"Speed Booster": ItemData(8678012, "Equipment", ItemClassification.progression, 7, 1),
"Screw Attack": ItemData(8678013, "Equipment", ItemClassification.progression, 8, 1),
"Varia Suit": ItemData(8678014, "Equipment", ItemClassification.useful, 5, 1),
"Gravity Suit": ItemData(8678015, "Equipment", ItemClassification.progression, 9, 1),
"Charge Beam": ItemData(8678016, "Beam", ItemClassification.useful, 10, 1),
"Wave Beam": ItemData(8678017, "Beam", ItemClassification.useful, 12, 1),
"Spazer": ItemData(8678018, "Beam", ItemClassification.useful, 13, 1),
"Plasma Beam": ItemData(8678019, "Beam", ItemClassification.useful, 14, 1),
"Ice Beam": ItemData(8678020, "Beam", ItemClassification.progression, 11, 1),
"Equipment Trap": ItemData(8678021, "Trap", ItemClassification.trap),
"Freeze Trap": ItemData(8678022, "Trap", ItemClassification.trap),
"Short Beam": ItemData(8678023, "Trap", ItemClassification.trap),
"EMP Trap": ItemData(8678024, "Trap", ItemClassification.trap),
"Metroid": ItemData(8678025, "MacGuffin", ItemClassification.progression_skip_balancing),
"Metroid": ItemData(8678025, "MacGuffin", ItemClassification.progression_skip_balancing, 19),
"The Galaxy is at Peace": ItemData(8678026, "Victory", ItemClassification.progression)

}
Expand Down Expand Up @@ -139,6 +140,8 @@ def item_is_trap(item_name: str) -> bool:

item_name_to_id: Dict[str, int] = {name: data.code for name, data in item_table.items()}



item_name_groups: Dict[str, Set[str]] = {
group: set(item_names) for group, item_names in itertools.groupby(item_table, get_item_group)
}
Loading

0 comments on commit 61ae729

Please sign in to comment.