diff --git a/Main.py b/Main.py index c627e7d4..39efede3 100644 --- a/Main.py +++ b/Main.py @@ -363,6 +363,7 @@ def main(args, seed=None): multidatatags.append("Spoiler") if not args.skip_playthrough: multidatatags.append("Play through") + minimum_versions = {"server": (1,0,0)} multidata = zlib.compress(json.dumps({"names": parsed_names, # backwards compat for < 2.4.1 "roms": [(slot, team, list(name.encode())) @@ -379,7 +380,8 @@ def main(args, seed=None): "er_hint_data": er_hint_data, "precollected_items": precollected_items, "version": _version_tuple, - "tags": multidatatags + "tags": multidatatags, + "minimum_versions" : minimum_versions }).encode("utf-8"), 9) with open(output_path('%s.multidata' % outfilebase), 'wb') as f: diff --git a/MultiServer.py b/MultiServer.py index 5503a61c..84978f4c 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -112,6 +112,7 @@ class Context(Node): self.auto_saver_thread = None self.save_dirty = False self.tags = ['Berserker'] + self.minimum_client_versions: typing.Dict[typing.Tuple[int, int], Utils.Version] = {} def load(self, multidatapath: str, use_embedded_server_options: bool = False): with open(multidatapath, 'rb') as f: @@ -121,6 +122,16 @@ class Context(Node): self.data_filename = multidatapath def _load(self, jsonobj: dict, use_embedded_server_options: bool): + if "minimum_versions" in jsonobj: + mdata_ver = tuple(jsonobj["minimum_versions"]["server"]) + if mdata_ver > Utils._version_tuple: + raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver}," + f"however this server is of version {Utils._version_tuple}") + clients_ver = jsonobj["minimum_versions"].get("clients", []) + self.minimum_client_versions = {} + for team, player, version in clients_ver: + self.minimum_client_versions[team, player] = Utils.Version(*version) + for team, names in enumerate(jsonobj['names']): for player, name in enumerate(names, 1): self.player_names[(team, player)] = name @@ -996,9 +1007,14 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args): client.name = ctx.player_names[(team, slot)] client.team = team client.slot = slot + minver = Utils.Version(*(ctx.minimum_client_versions.get((team, slot), (0,0,0)))) + if minver > tuple(args.get('version', Client.version)): + errors.add('IncompatibleVersion') + if ctx.compatibility == 1 and "Berserker" not in args.get('tags', Client.tags): errors.add('IncompatibleVersion') - elif ctx.compatibility == 0 and args.get('version', Client.version) != list(_version_tuple): + #only exact version match allowed + elif ctx.compatibility == 0 and tuple(args.get('version', Client.version)) != _version_tuple: errors.add('IncompatibleVersion') if errors: logging.info(f"A client connection was refused due to: {errors}") diff --git a/Utils.py b/Utils.py index 98109239..12bc9278 100644 --- a/Utils.py +++ b/Utils.py @@ -3,7 +3,12 @@ import typing def tuplize_version(version: str) -> typing.Tuple[int, ...]: - return tuple(int(piece, 10) for piece in version.split(".")) + return Version(*(int(piece, 10) for piece in version.split("."))) + +class Version(typing.NamedTuple): + major: int + minor: int + micro: int __version__ = "3.4.2"