Merge branch 'main' into breaking_changes

# Conflicts:
#	Adjuster.py
#	AdjusterMain.py
#	BaseClasses.py
#	MultiClient.py
#	MultiServer.py
#	Mystery.py
#	Utils.py
#	WebHostLib/downloads.py
#	WebHostLib/generate.py
#	dumpSprites.py
#	test/TestBase.py
#	worlds/alttp/EntranceRandomizer.py
#	worlds/alttp/Main.py
#	worlds/alttp/Rom.py
This commit is contained in:
Fabian Dill
2021-01-03 13:13:59 +01:00
558 changed files with 13839 additions and 3095 deletions

View File

@@ -30,7 +30,7 @@ from Utils import get_item_name_from_id, get_location_name_from_address, \
ReceivedItem, _version_tuple, restricted_loads
from NetUtils import Node, Endpoint
console_names = frozenset(set(Items.item_table) | set(Regions.location_table) | set(Items.item_name_groups))
console_names = frozenset(set(Items.item_table) | set(Regions.location_table) | set(Items.item_name_groups) | set(Regions.key_drop_data))
CLIENT_PLAYING = 0
CLIENT_GOAL = 1
@@ -59,6 +59,15 @@ class Client(Endpoint):
class Context(Node):
simple_options = {"hint_cost": int,
"location_check_points": int,
"server_password": str,
"password": str,
"forfeit_mode": str,
"remaining_mode": str,
"item_cheat": bool,
"compatibility": int}
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2):
@@ -104,6 +113,7 @@ class Context(Node):
self.auto_saver_thread = None
self.save_dirty = False
self.tags = ['AP']
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:
@@ -113,6 +123,16 @@ class Context(Node):
self.data_filename = multidatapath
def _load(self, decoded_obj: 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(decoded_obj['names']):
for player, name in enumerate(names, 1):
self.player_names[(team, player)] = name
@@ -127,15 +147,23 @@ class Context(Node):
self._set_options(server_options)
def _set_options(self, server_options: dict):
sentinel = object()
for key, value in server_options.items():
if key not in self.embedded_blacklist:
current = getattr(self, key, sentinel)
if current is not sentinel:
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
setattr(self, key, value)
self.item_cheat = not server_options.get("disable_item_cheat", True)
data_type = self.simple_options.get(key, None)
if data_type is not None:
if value not in {False, True, None}: # some can be boolean OR text, such as password
try:
value = data_type(value)
except Exception as e:
try:
raise Exception(f"Could not set server option {key}, skipping.") from e
except Exception as e:
logging.exception(e)
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
setattr(self, key, value)
elif key == "disable_item_cheat":
self.item_cheat = not bool(value)
else:
logging.debug(f"Unrecognized server option {key}")
def save(self, now=False) -> bool:
if self.saving:
@@ -435,6 +463,7 @@ def send_new_items(ctx: Context):
def forfeit_player(ctx: Context, team: int, slot: int):
all_locations = {values[0] for values in Regions.location_table.values() if type(values[0]) is int}
all_locations.update({values[1] for values in Regions.key_drop_data.values()})
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
register_location_checks(ctx, team, slot, all_locations)
@@ -450,10 +479,12 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
def register_location_checks(ctx: Context, team: int, slot: int, locations):
found_items = False
new_locations = set(locations) - ctx.location_checks[team, slot]
known_locations = set()
if new_locations:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
for location in new_locations:
if (location, slot) in ctx.locations:
known_locations.add(location)
target_item, target_player = ctx.locations[(location, slot)]
if target_player != slot or slot in ctx.remote_items:
found = False
@@ -478,7 +509,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations):
if client.team == team and client.wants_item_notification:
asyncio.create_task(
ctx.send_msgs(client, [['ItemFound', (target_item, location, slot)]]))
ctx.location_checks[team, slot] |= new_locations
ctx.location_checks[team, slot] |= known_locations
send_new_items(ctx)
if found_items:
@@ -510,7 +541,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[Utils.Hint]:
hints = []
seeked_location = Regions.location_table[location][0]
seeked_location = Regions.lookup_name_to_id[location]
for check, result in ctx.locations.items():
location_id, finding_player = check
if finding_player == slot and location_id == seeked_location:
@@ -644,15 +675,6 @@ class CommandProcessor(metaclass=CommandMeta):
class CommonCommandProcessor(CommandProcessor):
ctx: Context
simple_options = {"hint_cost": int,
"location_check_points": int,
"server_password": str,
"password": str,
"forfeit_mode": str,
"item_cheat": bool,
"auto_save_interval": int,
"compatibility": int}
def _cmd_countdown(self, seconds: str = "10") -> bool:
"""Start a countdown in seconds"""
try:
@@ -665,7 +687,7 @@ class CommonCommandProcessor(CommandProcessor):
def _cmd_options(self):
"""List all current options. Warning: lists password."""
self.output("Current options:")
for option in self.simple_options:
for option in self.ctx.simple_options:
if option == "server_password" and self.marker == "!": #Do not display the server password to the client.
self.output(f"Option server_password is set to {('*' * random.randint(4,16))}")
else:
@@ -802,10 +824,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
def _cmd_missing(self) -> bool:
"""List all missing location checks from the server's perspective"""
locations = []
for location_id, location_name in Regions.lookup_id_to_name.items(): # cheat console is -1, keep in mind
if location_id != -1 and location_id not in self.ctx.location_checks[self.client.team, self.client.slot]:
locations.append(location_name)
locations = get_missing_checks(self.ctx, self.client)
if len(locations) > 0:
texts = [f'Missing: {location}\n' for location in locations]
@@ -930,6 +949,15 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(response)
return False
def get_missing_checks(ctx: Context, client: Client) -> list:
locations = []
#for location_id in [k[0] for k, v in ctx.locations if k[1] == client.slot]:
# if location_id not in ctx.location_checks[client.team, client.slot]:
# locations.append(Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}'))
for location_id, location_name in Regions.lookup_id_to_name.items(): # cheat console is -1, keep in mind
if location_id != -1 and location_id not in ctx.location_checks[client.team, client.slot] and (location_id, client.slot) in ctx.locations:
locations.append(location_name)
return locations
def get_client_points(ctx: Context, client: Client) -> int:
return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) -
@@ -972,9 +1000,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 "AP" 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}")
@@ -986,7 +1019,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
client.tags = args.get('tags', Client.tags)
reply = [['Connected', [(client.team, client.slot),
[(p, ctx.get_aliased_name(t, p)) for (t, p), n in ctx.player_names.items() if
t == client.team]]]]
t == client.team], get_missing_checks(ctx, client)]]]
items = get_received_items(ctx, client.team, client.slot)
if items:
reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
@@ -1215,7 +1248,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
def _cmd_option(self, option_name: str, option: str):
"""Set options for the server. Warning: expires on restart"""
attrtype = self.simple_options.get(option_name, None)
attrtype = self.ctx.simple_options.get(option_name, None)
if attrtype:
if attrtype == bool:
def attrtype(input_text: str):
@@ -1229,7 +1262,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
return True
else:
known = (f"{option}:{otype}" for option, otype in self.simple_options.items())
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
self.output(f"Unrecognized Option {option_name}, known: "
f"{', '.join(known)}")
return False