Core: change how required versions work, deprecate IgnoreGame ()

`AutoWorld.World`s can set required_server_version and required_client_version properties. Drop `get_required_client_version()`.
`MultiServer` will set an absolute minimum client version based on its capability (protocol level).
`IgnoreVersion` tag is replaced by using `Tracker` or `TextOnly` with empty or null `game`.
Ignoring game will also ignore game's required_client_version (and fall back to server capability).
This commit is contained in:
black-sliver 2022-04-08 11:16:36 +02:00 committed by GitHub
parent 0acca6dd64
commit 42fecc7491
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 55 additions and 55 deletions

View File

@ -600,7 +600,7 @@ if __name__ == '__main__':
class TextContext(CommonContext):
tags = {"AP", "IgnoreGame", "TextOnly"}
game = "Archipelago"
game = "" # empty matches any game since 0.3.2
items_handling = 0 # don't receive any NetworkItems
async def server_auth(self, password_requested: bool = False):

View File

@ -326,11 +326,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
slot_data = {}
client_versions = {}
games = {}
minimum_versions = {"server": (0, 2, 4), "clients": client_versions}
minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions}
slot_info = {}
names = [[name for player, name in sorted(world.player_name.items())]]
for slot in world.player_ids:
client_versions[slot] = world.worlds[slot].get_required_client_version()
player_world: AutoWorld.World = world.worlds[slot]
minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version)
client_versions[slot] = player_world.required_client_version
games[slot] = world.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
world.player_types[slot])

View File

@ -37,6 +37,7 @@ from Utils import get_item_name_from_id, get_location_name_from_id, \
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
SlotType
min_client_version = Version(0, 1, 6)
colorama.init()
# functions callable on storable data on the server by clients
@ -301,7 +302,7 @@ class Context:
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
self.minimum_client_versions = {}
for player, version in clients_ver.items():
self.minimum_client_versions[player] = Utils.Version(*version)
self.minimum_client_versions[player] = max(Utils.Version(*version), min_client_version)
self.clients = {}
for team, names in enumerate(decoded_obj['names']):
@ -1388,9 +1389,11 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
else:
team, slot = ctx.connect_names[args['name']]
game = ctx.games[slot]
if "IgnoreGame" not in args["tags"] and args['game'] != game:
ignore_game = "IgnoreGame" in args["tags"] or ( # IgnoreGame is deprecated. TODO: remove after 0.3.3?
("TextOnly" in args["tags"] or "Tracker" in args["tags"]) and not args.get("game"))
if not ignore_game and args['game'] != game:
errors.add('InvalidGame')
minver = ctx.minimum_client_versions[slot]
minver = min_client_version if ignore_game else ctx.minimum_client_versions[slot]
if minver > args['version']:
errors.add('IncompatibleVersion')
if args.get('items_handling', None) is None:

View File

@ -398,7 +398,7 @@ The world has to provide the following things for generation
`self.world.get_filled_locations(self.player)` will filter for this world.
`item.player` can be used to see if it's a local item.
In addition the following methods can be implemented
In addition, the following methods can be implemented and attributes can be set
* `def generate_early(self)`
called per player before any items or locations are created. You can set
@ -418,11 +418,9 @@ In addition the following methods can be implemented
before, during and after the regular fill process, before `generate_output`.
* `fill_slot_data` and `modify_multidata` can be used to modify the data that
will be used by the server to host the MultiWorld.
* `def get_required_client_version(self)`
can return a tuple of 3 ints to make sure the client is compatible to this
world (e.g. item IDs) when connecting.
Always use `return max((x,y,z), super().get_required_client_version())`
to catch updates in the lower layers that break compatibility.
* `required_client_version: Tuple(int, int, int)`
Client version as tuple of 3 ints to make sure the client is compatible to
this world (e.g. implements all required features) when connecting.
#### generate_early

View File

@ -583,13 +583,13 @@ GameData is a **dict** but contains these keys and values. It's broken out into
### Tags
Tags are represented as a list of strings, the common Client tags follow:
| Name | Notes |
| ----- | ---- |
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
| IgnoreGame | Tells the server to ignore the "game" attribute in the [Connect](#Connect) packet. |
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets |
| Tracker | Tells the server that this client is actually a Tracker, will refuse new locations from this client and send all items as if they were remote items. |
| TextOnly | Tells the server that this client will not send locations and does not want to receive items. |
| Name | Notes |
|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
| IgnoreGame | Deprecated. See Tracker and TextOnly. Tells the server to ignore the "game" attribute in the [Connect](#Connect) packet. |
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets |
| Tracker | Tells the server that this client will not send locations and is actually a Tracker. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
| TextOnly | Tells the server that this client will not send locations and is intended for chat. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
### DeathLink
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:

View File

@ -27,6 +27,15 @@ class AutoWorldRegister(type):
dct["location_names"] = frozenset(dct["location_name_to_id"])
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
# move away from get_required_client_version function
if "game" in dct:
assert "get_required_client_version" not in dct, f"{name}: required_client_version is an attribute now"
# set minimum required_client_version from bases
if "required_client_version" in dct and bases:
for base in bases:
if "required_client_version" in base.__dict__:
dct["required_client_version"] = max(dct["required_client_version"], base.required_client_version)
# construct class
new_class = super().__new__(cls, name, bases, dct)
if "game" in dct:
@ -106,6 +115,14 @@ class World(metaclass=AutoWorldRegister):
# retrieved by clients on every connection.
data_version: int = 1
# override this if changes to a world break forward-compatibility of the client
# The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
# future. Protocol level compatibility check moved to MultiServer.min_client_version.
required_client_version: Tuple[int, int, int] = (0, 1, 6)
# update this if the resulting multidata breaks forward-compatibility of the server
required_server_version: Tuple[int, int, int] = (0, 2, 4)
hint_blacklist: Set[str] = frozenset() # any names that should not be hintable
# NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set.
@ -191,9 +208,6 @@ class World(metaclass=AutoWorldRegister):
"""For deeper modification of server multidata."""
pass
def get_required_client_version(self) -> Tuple[int, int, int]:
return 0, 1, 6
# Spoiler writing is optional, these may not get called.
def write_spoiler_header(self, spoiler_handle: TextIO):
"""Write to the spoiler header. If individual it's right at the end of that player's options,

View File

@ -43,6 +43,7 @@ class ALTTPWorld(World):
data_version = 8
remote_items: bool = False
remote_start_inventory: bool = False
required_client_version = (0, 3, 2)
set_rules = set_rules
@ -324,9 +325,6 @@ class ALTTPWorld(World):
new_name = base64.b64encode(bytes(self.rom_name)).decode()
multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]]
def get_required_client_version(self) -> tuple:
return max((0, 3, 2), super(ALTTPWorld, self).get_required_client_version())
def create_item(self, name: str) -> Item:
return ALttPItem(name, self.player, **as_dict_item_table[name])

View File

@ -42,6 +42,7 @@ class Factorio(World):
"Progressive": set(progressive_tech_table.values()),
}
data_version = 5
required_client_version = (0, 3, 0)
def __init__(self, world, player: int):
super(Factorio, self).__init__(world, player)
@ -179,9 +180,6 @@ class Factorio(World):
return super(Factorio, self).collect_item(state, item, remove)
def get_required_client_version(self) -> tuple:
return max((0, 2, 6), super(Factorio, self).get_required_client_version())
options = factorio_options
@classmethod

View File

@ -32,6 +32,9 @@ class MeritousWorld(World):
data_version = 2
forced_auto_forfeit = False
# NOTE: Remember to change this before this game goes live
required_client_version = (0, 2, 4)
options = meritous_options
def __init__(self, world: MultiWorld, player: int):
@ -150,10 +153,6 @@ class MeritousWorld(World):
self.world.completion_condition[self.player] = lambda state: state.has(
"Full Victory", self.player)
def get_required_client_version(self) -> tuple:
# NOTE: Remember to change this before this game goes live
return max((0, 2, 4), super(MeritousWorld, self).get_required_client_version())
def fill_slot_data(self) -> dict:
return {
"goal": self.goal,

View File

@ -27,6 +27,7 @@ class RaftWorld(World):
options = raft_options
data_version = 1
required_client_version = (0, 2, 0)
def generate_basic(self):
minRPSpecified = self.world.minimum_resource_pack_amount[self.player].value
@ -111,9 +112,6 @@ class RaftWorld(World):
return super(RaftWorld, self).collect_item(state, item, remove)
def get_required_client_version(self) -> typing.Tuple[int, int, int]:
return max((0, 2, 0), super(RaftWorld, self).get_required_client_version())
def pre_fill(self):
if self.world.island_frequency_locations[self.player] == 0:
self.setLocationItem("Radio Tower Frequency to Vasagatan", "Vasagatan Frequency")

View File

@ -21,6 +21,7 @@ class LegacyWorld(World):
options = legacy_options
topology_present = False
data_version = 3
required_client_version = (0, 2, 3)
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = location_table
@ -54,9 +55,6 @@ class LegacyWorld(World):
data = item_table[name]
return [self.create_item(name)] * data.quantity
def get_required_client_version(self) -> typing.Tuple[int, int, int]:
return max((0, 2, 3), super(LegacyWorld, self).get_required_client_version())
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
for option_name in legacy_options:

View File

@ -69,6 +69,10 @@ class SMWorld(World):
remote_items: bool = False
remote_start_inventory: bool = False
# changes to client DeathLink handling for 0.2.1
# changes to client Remote Item handling for 0.2.6
required_client_version = (0, 2, 6)
itemManager: ItemManager
locations = {}
@ -167,11 +171,6 @@ class SMWorld(World):
create_locations(self, self.player)
create_regions(self, self.world, self.player)
def get_required_client_version(self):
# changes to client DeathLink handling for 0.2.1
# changes to client Remote Item handling for 0.2.6
return max(super(SMWorld, self).get_required_client_version(), (0, 2, 6))
def getWord(self, w):
return (w & 0x00FF, (w & 0xFF00) >> 8)

View File

@ -51,6 +51,9 @@ class SMZ3World(World):
remote_items: bool = False
remote_start_inventory: bool = False
# first added for 0.2.6
required_client_version = (0, 2, 6)
def __init__(self, world: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
self.locations = {}
@ -135,10 +138,6 @@ class SMZ3World(World):
startRegion.exits.append(exit)
exit.connect(currentRegion)
def get_required_client_version(self):
# first added for 0.2.6
return max(super(SMZ3World, self).get_required_client_version(), (0, 2, 6))
def apply_sm_custom_sprite(self):
itemSprites = ["off_world_prog_item.bin", "off_world_item.bin"]
itemSpritesAddress = [0xF800, 0xF900]

View File

@ -147,6 +147,7 @@ class SoEWorld(World):
remote_items = False
data_version = 2
web = SoEWebWorld()
required_client_version = (0, 2, 6)
item_name_to_id, item_id_to_raw = _get_item_mapping()
location_name_to_id, location_id_to_raw = _get_location_mapping()
@ -321,10 +322,6 @@ class SoEWorld(World):
payload = multidata["connect_names"][self.world.player_name[self.player]]
multidata["connect_names"][self.connect_name] = payload
def get_required_client_version(self):
return max((0, 2, 6), super(SoEWorld, self).get_required_client_version())
class SoEItem(Item):
game: str = "Secret of Evermore"

View File

@ -28,6 +28,7 @@ class SubnauticaWorld(World):
options = options
data_version = 2
required_client_version = (0, 1, 9)
def generate_basic(self):
# Link regions
@ -77,10 +78,6 @@ class SubnauticaWorld(World):
item = lookup_name_to_item[name]
return SubnauticaItem(name, item["progression"], item["id"], player=self.player)
def get_required_client_version(self) -> typing.Tuple[int, int, int]:
return max((0, 1, 9), super(SubnauticaWorld, self).get_required_client_version())
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, None, name, player)
ret.world = world