* 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()
 |