From f0cfe30a3658221d259758966ef1607e9aa93dcb Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 23 Jan 2022 06:38:46 +0100 Subject: [PATCH] Move remote_items and _start_inventory from world to client (#227) --- CommonClient.py | 8 ++++-- FF1Client.py | 5 ++-- FactorioClient.py | 1 + MultiServer.py | 55 +++++++++++++++++++++++++++++----------- SNIClient.py | 3 +++ docs/network protocol.md | 16 ++++++++++-- 6 files changed, 67 insertions(+), 21 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index d8426c41..a89a2703 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -118,6 +118,7 @@ class CommonContext(): game = None ui = None keep_alive_task = None + items_handling: typing.Optional[int] = None def __init__(self, server_address, password): # server state @@ -247,9 +248,9 @@ class CommonContext(): async def send_connect(self, **kwargs): payload = { - "cmd": 'Connect', + 'cmd': 'Connect', 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, - 'tags': self.tags, + 'tags': self.tags, 'items_handling': self.items_handling, 'uuid': Utils.get_unique_identifier(), 'game': self.game } if kwargs: @@ -469,6 +470,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict): logger.error('Invalid password') ctx.password = None await ctx.server_auth(True) + elif 'InvalidItemsHandling' in errors: + raise Exception('The item handling flags requested by the client are not supported') elif errors: raise Exception("Unknown connection errors: " + str(errors)) else: @@ -589,6 +592,7 @@ if __name__ == '__main__': class TextContext(CommonContext): tags = {"AP", "IgnoreGame", "TextOnly"} game = "Archipelago" + items_handling = 0 # don't receive any NetworkItems async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: diff --git a/FF1Client.py b/FF1Client.py index 90d2b484..23e2a8be 100644 --- a/FF1Client.py +++ b/FF1Client.py @@ -30,6 +30,9 @@ class FF1CommandProcessor(ClientCommandProcessor): class FF1Context(CommonContext): + command_processor = FF1CommandProcessor + items_handling = 0b111 # full remote + def __init__(self, server_address, password): super().__init__(server_address, password) self.nes_streams: (StreamReader, StreamWriter) = None @@ -40,8 +43,6 @@ class FF1Context(CommonContext): self.game = 'Final Fantasy' self.awaiting_rom = False - command_processor = FF1CommandProcessor - async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: await super(FF1Context, self).server_auth(password_requested) diff --git a/FactorioClient.py b/FactorioClient.py index 825e1052..a9659e85 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -49,6 +49,7 @@ class FactorioCommandProcessor(ClientCommandProcessor): class FactorioContext(CommonContext): command_processor = FactorioCommandProcessor game = "Factorio" + items_handling = 0b111 # full remote # updated by spinup server mod_version: Utils.Version = Utils.Version(0, 0, 0) diff --git a/MultiServer.py b/MultiServer.py index af7a4fbc..8bb41ef3 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -56,11 +56,19 @@ class Client(Endpoint): self.messageprocessor = client_message_processor(ctx, self) self.ctx = weakref.ref(ctx) - def parse_tags(self, ctx: Context): - self.remote_items = 'Tracker' in self.tags or self.slot in ctx.remote_items - self.remote_start_inventory = 'Tracker' in self.tags or self.slot in ctx.remote_start_inventory - self.no_items = 'TextOnly' in self.tags - self.no_locations = 'TextOnly' in self.tags or 'Tracker' in self.tags + @property + def items_handling(self): + if self.no_items: + return 0 + return 1 + (self.remote_items << 1) + (self.remote_start_inventory << 2) + + @items_handling.setter + def items_handling(self, value: int): + if not (value & 0b001) and (value & 0b110): + raise ValueError("Invalid flag combination") + self.no_items = not (value & 0b001) + self.remote_items = bool(value & 0b010) + self.remote_start_inventory = bool(value & 0b100) @property def name(self) -> str: @@ -1307,6 +1315,16 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): minver = ctx.minimum_client_versions[slot] if minver > args['version']: errors.add('IncompatibleVersion') + if args.get('items_handling', None) is None: + # fall back to load from multidata + client.no_items = False + client.remote_items = slot in ctx.remote_items + client.remote_start_inventory = slot in ctx.remote_start_inventory + else: + try: + client.items_handling = args['items_handling'] + except (ValueError, TypeError): + errors.add('InvalidItemsHandling') # only exact version match allowed if ctx.compatibility == 0 and args['version'] != version_tuple: @@ -1327,7 +1345,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): ctx.clients[team][slot].append(client) client.version = args['version'] client.tags = args['tags'] - client.parse_tags(ctx) + client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags reply = [{ "cmd": "Connected", "team": client.team, "slot": client.slot, @@ -1338,7 +1356,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): }] start_inventory = get_start_inventory(ctx, team, slot, client.remote_start_inventory) items = get_received_items(ctx, client.team, client.slot, client.remote_items) - if start_inventory or items: + if (start_inventory or items) and not client.no_items: reply.append({"cmd": 'ReceivedItems', "index": 0, "items": start_inventory + items}) client.send_index = len(start_inventory) + len(items) if not client.auth: # if this was a Re-Connect, don't print to console @@ -1368,21 +1386,28 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): "original_cmd": cmd}]) return - if "tags" in args: - old_tags = client.tags - client.tags = args["tags"] - client.parse_tags(ctx) - if "Tracker" in old_tags != "Tracker" in client.tags \ - or "TextOnly" in old_tags != "TextOnly" in client.tags: + if args.get('items_handling', None) is not None and client.items_handling != args['items_handling']: + try: + client.items_handling = args['items_handling'] start_inventory = get_start_inventory(ctx, client.team, client.slot, client.remote_start_inventory) items = get_received_items(ctx, client.team, client.slot, client.remote_items) - if start_inventory or items: + if (items or start_inventory) and not client.no_items: client.send_index = len(start_inventory) + len(items) await ctx.send_msgs(client, [{"cmd": "ReceivedItems", "index": 0, "items": start_inventory + items}]) else: client.send_index = 0 + except (ValueError, TypeError) as err: + await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', 'type': 'arguments', + 'text': f'Invalid items_handling: {err}', + 'original_cmd': cmd}]) + return + + if "tags" in args: + old_tags = client.tags + client.tags = args["tags"] if set(old_tags) != set(client.tags): + client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags ctx.notify_all( f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags " f"from {old_tags} to {client.tags}.") @@ -1390,7 +1415,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): elif cmd == 'Sync': start_inventory = get_start_inventory(ctx, client.team, client.slot, client.remote_start_inventory) items = get_received_items(ctx, client.team, client.slot, client.remote_items) - if start_inventory or items: + if (start_inventory or items) and not client.no_items: client.send_index = len(start_inventory) + len(items) await ctx.send_msgs(client, [{"cmd": "ReceivedItems", "index": 0, "items": start_inventory + items}]) diff --git a/SNIClient.py b/SNIClient.py index af63b1fa..58c7957f 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -102,6 +102,7 @@ class LttPCommandProcessor(ClientCommandProcessor): class Context(CommonContext): command_processor = LttPCommandProcessor game = "A Link to the Past" + items_handling = None # set in game_watcher def __init__(self, snes_address, server_address, password): super(Context, self).__init__(server_address, password) @@ -901,8 +902,10 @@ async def game_watcher(ctx: Context): continue elif game_name == b"SM": ctx.game = GAME_SM + ctx.items_handling = 0b001 # full local else: ctx.game = GAME_ALTTP + ctx.items_handling = 0b001 # full local rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else ROMNAME_START, ROMNAME_SIZE) if rom is None or rom == bytes([0] * ROMNAME_SIZE): diff --git a/docs/network protocol.md b/docs/network protocol.md index f7c65538..435f2888 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -99,13 +99,14 @@ Sent to clients when the server refuses connection. This is sent during the init #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `InvalidGame`, `SlotAlreadyTaken`, `IncompatibleVersion`, or `InvalidPassword`. | +| errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `InvalidGame`, `SlotAlreadyTaken`, `IncompatibleVersion`, `InvalidPassword`, or `InvalidItemsHandling`. | InvalidSlot indicates that the sent 'name' field did not match any auth entry on the server. InvalidGame indicates that a correctly named slot was found, but the game for it mismatched. SlotAlreadyTaken indicates a connection with a different uuid is already established. IncompatibleVersion indicates a version mismatch. InvalidPassword indicates the wrong, or no password when it was required, was sent. +InvalidItemsHandling indicates a wrong value type or flag combination was sent. ### Connected Sent to clients when the connection handshake is successfully completed. @@ -214,17 +215,28 @@ Sent by the client to initiate a connection to an Archipelago game session. | name | str | The player name for this client. | | uuid | str | Unique identifier for player client. | | version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. | +| items_handling | int | Flags configuring which items should be sent by the server. Read below for individual flags. | tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) | +#### items_handling flags +| Value | Meaning | +| ----- | ------- | +| 0b000 | No ReceivedItems is sent to you, ever. | +| 0b001 | Indicates you get items sent from other worlds. | +| 0b010 | Indicates you get items sent from your own world. Requires 0b001 to be set. | +| 0b100 | Indicates you get your starting inventory sent. Requires 0b001 to be set. | +| null | Null or undefined loads settings from world definition for backwards compatibility. This is deprecated. | + #### Authentication Many, if not all, other packets require a successfully authenticated client. This is described in more detail in [Archipelago Connection Handshake](#Archipelago-Connection-Handshake). ### ConnectUpdate -Update arguments from the Connect package, currently only updating tags is supported. +Update arguments from the Connect package, currently only updating tags and items_handling is supported. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | +| items_handling | int | Flags configuring which items should be sent by the server. | tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) | ### Sync