mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
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:
@@ -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
|
||||
|
Reference in New Issue
Block a user