 97ca2ad258
			
		
	
	97ca2ad258
	
	
	
		
			
			* duh * Fuck it * Major fixes * a * b * Even more fixes * New option - NoFreeRoamFinale * a * Hat Logic Fix * Just to be safe * multiworld.random to world.random * KeyError fix * Update .gitignore * Update __init__.py * Zoinks Scoob * ffs * Ruh Roh Raggy, more r-r-r-random bugs! * 0.9b - cleanup + expanded logic difficulty * Update Rules.py * Update Regions.py * AttributeError fix * 0.10b - New Options * 1.0 Preparations * Docs * Docs 2 * Fixes * Update __init__.py * Fixes * variable capture my beloathed * Fixes * a * 10 Seconds logic fix * 1.1 * 1.2 * a * New client * More client changes * 1.3 * Final touch-ups for 1.3 * 1.3.1 * 1.3.3 * Zero Jumps gen error fix * more fixes * Formatting improvements * typo * Update __init__.py * Revert "Update __init__.py" This reverts commit e178a7c0a6904ace803241cab3021d7b97177e90. * init * Update to new options API * Missed some * Snatcher Coins fix * Missed some more * some slight touch ups * rewind * a * fix things * Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit" This reverts commit a2360fe197e77a723bb70006c5eb5725c7ed3826, reversing changes made to b8948bc4958855c6e342e18bdb8dc81cfcf09455. * Update .gitignore * 1.3.6 * Final touch-ups * Fix client and leftover old options api * Delete setup-ahitclient.py * Update .gitignore * old python version fix * proper warnings for invalid act plandos * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * Update worlds/ahit/docs/setup_en.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * 120 char per line * "settings" to "options" * Update DeathWishRules.py * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * No more loading the data package * cleanup + act plando fixes * almost forgot * Update Rules.py * a * Update worlds/ahit/Options.py Co-authored-by: Ixrec <ericrhitchcock@gmail.com> * Options stuff * oop * no unnecessary type hints * warn about depot download length in setup guide * Update worlds/ahit/Options.py Co-authored-by: Ixrec <ericrhitchcock@gmail.com> * typo Co-authored-by: Ixrec <ericrhitchcock@gmail.com> * Update worlds/ahit/Rules.py Co-authored-by: Ixrec <ericrhitchcock@gmail.com> * review stuff * More stuff from review * comment * 1.5 Update * link fix? * link fix 2 * Update setup_en.md * Update setup_en.md * Update setup_en.md * Evil * Good fucking lord * Review stuff again + Logic fixes * More review stuff * Even more review stuff - we're almost done * DW review stuff * Finish up review stuff * remove leftover stuff * a * assert item * add A Hat in Time to readme/codeowners files * Fix range options not being corrected properly * 120 chars per line in docs * Update worlds/ahit/Regions.py Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com> * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com> * Remove some unnecessary option.class.value * Remove data_version and more option.class.value * Update worlds/ahit/Items.py Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com> * Remove the rest of option.class.value * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com> * review stuff * Replace connect_regions with Region.connect * review stuff * Remove unnecessary Optional from LocData * Remove HatType.NONE * Update worlds/ahit/test/TestActs.py Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com> * fix so default tests actually don't run * Improve performance for death wish rules * rename test file * change test imports * 1000 is probably unnecessary * a * change state.count to state.has * stuff * starting inventory hats fix * shouldn't have done this lol * make ship shape task goal equal to number of tasksanity checks if set to 0 * a * change act shuffle starting acts + logic updates * dumb * option groups + lambda capture cringe + typo * a * b * missing option in groups * c * Fix Your Contract Has Expired being placed on first level when it shouldn't * yche fix * formatting * major logic bug fix for death wish * Update Regions.py * Add missing indirect connections * Fix generation error from chapter 2 start with act shuffle off * a * Revert "a" This reverts commit df58bbcd998585760cc6ac9ea54b6fdf142b4fd1. * Revert "Fix generation error from chapter 2 start with act shuffle off" This reverts commit 0f4d441824af34bf7a7cff19f5f14161752d8661. * fix async lag * Update Client.py * shop item names need this now * fix indentation --------- Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Ixrec <ericrhitchcock@gmail.com> Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com> Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
		
			
				
	
	
		
			267 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			267 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import asyncio
 | |
| import Utils
 | |
| import websockets
 | |
| import functools
 | |
| from copy import deepcopy
 | |
| from typing import List, Any, Iterable
 | |
| from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem, NetworkPlayer
 | |
| from MultiServer import Endpoint
 | |
| from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser
 | |
| 
 | |
| DEBUG = False
 | |
| 
 | |
| 
 | |
| class AHITJSONToTextParser(JSONtoTextParser):
 | |
|     def _handle_color(self, node: JSONMessagePart):
 | |
|         return self._handle_text(node)  # No colors for the in-game text
 | |
| 
 | |
| 
 | |
| class AHITCommandProcessor(ClientCommandProcessor):
 | |
|     def _cmd_ahit(self):
 | |
|         """Check AHIT Connection State"""
 | |
|         if isinstance(self.ctx, AHITContext):
 | |
|             logger.info(f"AHIT Status: {self.ctx.get_ahit_status()}")
 | |
| 
 | |
| 
 | |
| class AHITContext(CommonContext):
 | |
|     command_processor = AHITCommandProcessor
 | |
|     game = "A Hat in Time"
 | |
| 
 | |
|     def __init__(self, server_address, password):
 | |
|         super().__init__(server_address, password)
 | |
|         self.proxy = None
 | |
|         self.proxy_task = None
 | |
|         self.gamejsontotext = AHITJSONToTextParser(self)
 | |
|         self.autoreconnect_task = None
 | |
|         self.endpoint = None
 | |
|         self.items_handling = 0b111
 | |
|         self.room_info = None
 | |
|         self.connected_msg = None
 | |
|         self.game_connected = False
 | |
|         self.awaiting_info = False
 | |
|         self.full_inventory: List[Any] = []
 | |
|         self.server_msgs: List[Any] = []
 | |
| 
 | |
|     async def server_auth(self, password_requested: bool = False):
 | |
|         if password_requested and not self.password:
 | |
|             await super(AHITContext, self).server_auth(password_requested)
 | |
| 
 | |
|         await self.get_username()
 | |
|         await self.send_connect()
 | |
| 
 | |
|     def get_ahit_status(self) -> str:
 | |
|         if not self.is_proxy_connected():
 | |
|             return "Not connected to A Hat in Time"
 | |
| 
 | |
|         return "Connected to A Hat in Time"
 | |
| 
 | |
|     async def send_msgs_proxy(self, msgs: Iterable[dict]) -> bool:
 | |
|         """ `msgs` JSON serializable """
 | |
|         if not self.endpoint or not self.endpoint.socket.open or self.endpoint.socket.closed:
 | |
|             return False
 | |
| 
 | |
|         if DEBUG:
 | |
|             logger.info(f"Outgoing message: {msgs}")
 | |
| 
 | |
|         await self.endpoint.socket.send(msgs)
 | |
|         return True
 | |
| 
 | |
|     async def disconnect(self, allow_autoreconnect: bool = False):
 | |
|         await super().disconnect(allow_autoreconnect)
 | |
| 
 | |
|     async def disconnect_proxy(self):
 | |
|         if self.endpoint and not self.endpoint.socket.closed:
 | |
|             await self.endpoint.socket.close()
 | |
|         if self.proxy_task is not None:
 | |
|             await self.proxy_task
 | |
| 
 | |
|     def is_connected(self) -> bool:
 | |
|         return self.server and self.server.socket.open
 | |
| 
 | |
|     def is_proxy_connected(self) -> bool:
 | |
|         return self.endpoint and self.endpoint.socket.open
 | |
| 
 | |
|     def on_print_json(self, args: dict):
 | |
|         text = self.gamejsontotext(deepcopy(args["data"]))
 | |
|         msg = {"cmd": "PrintJSON", "data": [{"text": text}], "type": "Chat"}
 | |
|         self.server_msgs.append(encode([msg]))
 | |
| 
 | |
|         if self.ui:
 | |
|             self.ui.print_json(args["data"])
 | |
|         else:
 | |
|             text = self.jsontotextparser(args["data"])
 | |
|             logger.info(text)
 | |
| 
 | |
|     def update_items(self):
 | |
|         # just to be safe - we might still have an inventory from a different room
 | |
|         if not self.is_connected():
 | |
|             return
 | |
| 
 | |
|         self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}]))
 | |
| 
 | |
|     def on_package(self, cmd: str, args: dict):
 | |
|         if cmd == "Connected":
 | |
|             json = args
 | |
|             # This data is not needed and causes the game to freeze for long periods of time in large asyncs.
 | |
|             if "slot_info" in json.keys():
 | |
|                 json["slot_info"] = {}
 | |
|             if "players" in json.keys():
 | |
|                 me: NetworkPlayer
 | |
|                 for n in json["players"]:
 | |
|                     if n.slot == json["slot"] and n.team == json["team"]:
 | |
|                         me = n
 | |
|                         break
 | |
| 
 | |
|                 # Only put our player info in there as we actually need it
 | |
|                 json["players"] = [me]
 | |
|             if DEBUG:
 | |
|                 print(json)
 | |
|             self.connected_msg = encode([json])
 | |
|             if self.awaiting_info:
 | |
|                 self.server_msgs.append(self.room_info)
 | |
|                 self.update_items()
 | |
|                 self.awaiting_info = False
 | |
| 
 | |
|         elif cmd == "RoomUpdate":
 | |
|             # Same story as above
 | |
|             json = args
 | |
|             if "players" in json.keys():
 | |
|                 json["players"] = []
 | |
| 
 | |
|             self.server_msgs.append(encode(json))
 | |
| 
 | |
|         elif cmd == "ReceivedItems":
 | |
|             if args["index"] == 0:
 | |
|                 self.full_inventory.clear()
 | |
| 
 | |
|             for item in args["items"]:
 | |
|                 self.full_inventory.append(NetworkItem(*item))
 | |
| 
 | |
|             self.server_msgs.append(encode([args]))
 | |
| 
 | |
|         elif cmd == "RoomInfo":
 | |
|             self.seed_name = args["seed_name"]
 | |
|             self.room_info = encode([args])
 | |
| 
 | |
|         else:
 | |
|             if cmd != "PrintJSON":
 | |
|                 self.server_msgs.append(encode([args]))
 | |
| 
 | |
|     def run_gui(self):
 | |
|         from kvui import GameManager
 | |
| 
 | |
|         class AHITManager(GameManager):
 | |
|             logging_pairs = [
 | |
|                 ("Client", "Archipelago")
 | |
|             ]
 | |
|             base_title = "Archipelago A Hat in Time Client"
 | |
| 
 | |
|         self.ui = AHITManager(self)
 | |
|         self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
 | |
| 
 | |
| 
 | |
| async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
 | |
|     ctx.endpoint = Endpoint(websocket)
 | |
|     try:
 | |
|         await on_client_connected(ctx)
 | |
| 
 | |
|         if ctx.is_proxy_connected():
 | |
|             async for data in websocket:
 | |
|                 if DEBUG:
 | |
|                     logger.info(f"Incoming message: {data}")
 | |
| 
 | |
|                 for msg in decode(data):
 | |
|                     if msg["cmd"] == "Connect":
 | |
|                         # Proxy is connecting, make sure it is valid
 | |
|                         if msg["game"] != "A Hat in Time":
 | |
|                             logger.info("Aborting proxy connection: game is not A Hat in Time")
 | |
|                             await ctx.disconnect_proxy()
 | |
|                             break
 | |
| 
 | |
|                         if ctx.seed_name:
 | |
|                             seed_name = msg.get("seed_name", "")
 | |
|                             if seed_name != "" and seed_name != ctx.seed_name:
 | |
|                                 logger.info("Aborting proxy connection: seed mismatch from save file")
 | |
|                                 logger.info(f"Expected: {ctx.seed_name}, got: {seed_name}")
 | |
|                                 text = encode([{"cmd": "PrintJSON",
 | |
|                                                 "data": [{"text": "Connection aborted - save file to seed mismatch"}]}])
 | |
|                                 await ctx.send_msgs_proxy(text)
 | |
|                                 await ctx.disconnect_proxy()
 | |
|                                 break
 | |
| 
 | |
|                         if ctx.auth:
 | |
|                             name = msg.get("name", "")
 | |
|                             if name != "" and name != ctx.auth:
 | |
|                                 logger.info("Aborting proxy connection: player name mismatch from save file")
 | |
|                                 logger.info(f"Expected: {ctx.auth}, got: {name}")
 | |
|                                 text = encode([{"cmd": "PrintJSON",
 | |
|                                                 "data": [{"text": "Connection aborted - player name mismatch"}]}])
 | |
|                                 await ctx.send_msgs_proxy(text)
 | |
|                                 await ctx.disconnect_proxy()
 | |
|                                 break
 | |
| 
 | |
|                         if ctx.connected_msg and ctx.is_connected():
 | |
|                             await ctx.send_msgs_proxy(ctx.connected_msg)
 | |
|                             ctx.update_items()
 | |
|                         continue
 | |
| 
 | |
|                     if not ctx.is_proxy_connected():
 | |
|                         break
 | |
| 
 | |
|                     await ctx.send_msgs([msg])
 | |
| 
 | |
|     except Exception as e:
 | |
|         if not isinstance(e, websockets.WebSocketException):
 | |
|             logger.exception(e)
 | |
|     finally:
 | |
|         await ctx.disconnect_proxy()
 | |
| 
 | |
| 
 | |
| async def on_client_connected(ctx: AHITContext):
 | |
|     if ctx.room_info and ctx.is_connected():
 | |
|         await ctx.send_msgs_proxy(ctx.room_info)
 | |
|     else:
 | |
|         ctx.awaiting_info = True
 | |
| 
 | |
| 
 | |
| async def proxy_loop(ctx: AHITContext):
 | |
|     try:
 | |
|         while not ctx.exit_event.is_set():
 | |
|             if len(ctx.server_msgs) > 0:
 | |
|                 for msg in ctx.server_msgs:
 | |
|                     await ctx.send_msgs_proxy(msg)
 | |
| 
 | |
|                 ctx.server_msgs.clear()
 | |
|             await asyncio.sleep(0.1)
 | |
|     except Exception as e:
 | |
|         logger.exception(e)
 | |
|         logger.info("Aborting AHIT Proxy Client due to errors")
 | |
| 
 | |
| 
 | |
| def launch():
 | |
|     async def main():
 | |
|         parser = get_base_parser()
 | |
|         args = parser.parse_args()
 | |
| 
 | |
|         ctx = AHITContext(args.connect, args.password)
 | |
|         logger.info("Starting A Hat in Time proxy server")
 | |
|         ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx),
 | |
|                                      host="localhost", port=11311, ping_timeout=999999, ping_interval=999999)
 | |
|         ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop")
 | |
| 
 | |
|         if gui_enabled:
 | |
|             ctx.run_gui()
 | |
|         ctx.run_cli()
 | |
| 
 | |
|         await ctx.proxy
 | |
|         await ctx.proxy_task
 | |
|         await ctx.exit_event.wait()
 | |
| 
 | |
|     Utils.init_logging("AHITClient")
 | |
|     # options = Utils.get_options()
 | |
| 
 | |
|     import colorama
 | |
|     colorama.init()
 | |
|     asyncio.run(main())
 | |
|     colorama.deinit()
 |