Client now uses location_id consistently internally, instead of switching on a dime all the time

And some smaller changes
This commit is contained in:
Fabian Dill 2021-03-07 22:05:07 +01:00
parent f7dc21ddcc
commit a528ed5e9e
7 changed files with 112 additions and 97 deletions

View File

@ -395,7 +395,8 @@ class MultiWorld():
while prog_locations: while prog_locations:
sphere = [] sphere = []
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres # build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in prog_locations: for location in prog_locations:
if location.can_reach(state): if location.can_reach(state):
sphere.append(location) sphere.append(location)
@ -433,8 +434,6 @@ class MultiWorld():
state.collect(location.item, True, location) state.collect(location.item, True, location)
locations -= sphere locations -= sphere
def fulfills_accessibility(self, state: Optional[CollectionState] = None): def fulfills_accessibility(self, state: Optional[CollectionState] = None):
"""Check if accessibility rules are fulfilled with current or supplied state.""" """Check if accessibility rules are fulfilled with current or supplied state."""
if not state: if not state:

View File

@ -147,6 +147,9 @@ def main(args, seed=None):
logger.info('') logger.info('')
for player in world.alttp_player_ids:
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids: for player in world.player_ids:
for tok in filter(None, args.startinventory[player].split(',')): for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.strip(), player) item = ItemFactory(tok.strip(), player)
@ -186,8 +189,6 @@ def main(args, seed=None):
hk_create_regions(world, player) hk_create_regions(world, player)
for player in world.alttp_player_ids: for player in world.alttp_player_ids:
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
if world.open_pyramid[player] == 'goal': if world.open_pyramid[player] == 'goal':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
elif world.open_pyramid[player] == 'auto': elif world.open_pyramid[player] == 'auto':

View File

@ -9,13 +9,12 @@ import socket
import os import os
import subprocess import subprocess
import base64 import base64
import re
import shutil import shutil
from json import loads, dumps from json import loads, dumps
from random import randrange from random import randrange
from Utils import get_item_name_from_id, get_location_name_from_address from Utils import get_item_name_from_id
exit_func = atexit.register(input, "Press enter to close.") exit_func = atexit.register(input, "Press enter to close.")
@ -54,6 +53,7 @@ class Context():
# WebUI Stuff # WebUI Stuff
self.ui_node = WebUI.WebUiClient() self.ui_node = WebUI.WebUiClient()
logger.addHandler(self.ui_node) logger.addHandler(self.ui_node)
self.ready = False
self.custom_address = None self.custom_address = None
self.webui_socket_port: typing.Optional[int] = port self.webui_socket_port: typing.Optional[int] = port
self.hint_cost = 0 self.hint_cost = 0
@ -70,7 +70,7 @@ class Context():
self.input_requests = 0 self.input_requests = 0
self.snes_socket = None self.snes_socket = None
self.snes_state = SNES_DISCONNECTED self.snes_state = SNESState.SNES_DISCONNECTED
self.snes_attached_device = None self.snes_attached_device = None
self.snes_reconnect_address = None self.snes_reconnect_address = None
self.snes_recv_queue = asyncio.Queue() self.snes_recv_queue = asyncio.Queue()
@ -87,12 +87,12 @@ class Context():
self.slot = None self.slot = None
self.player_names: typing.Dict[int: str] = {} self.player_names: typing.Dict[int: str] = {}
self.locations_recognized = set() self.locations_recognized = set()
# these should probably track IDs where possible
self.locations_checked:typing.Set[str] = set() self.locations_checked:typing.Set[int] = set()
self.locations_scouted:typing.Set[str] = set() self.locations_scouted:typing.Set[int] = set()
self.items_received = [] self.items_received = []
self.items_missing = [] self.missing_locations: typing.List[int] = []
self.items_checked = None self.checked_locations: typing.List[int] = []
self.locations_info = {} self.locations_info = {}
self.awaiting_rom = False self.awaiting_rom = False
self.rom = None self.rom = None
@ -414,6 +414,9 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400), 'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400),
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40), 'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
'Ganons Tower - Validation Chest': (0x4d, 0x10)} 'Ganons Tower - Validation Chest': (0x4d, 0x10)}
location_table_uw_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_uw.items()}
location_table_npc = {'Mushroom': 0x1000, location_table_npc = {'Mushroom': 0x1000,
'King Zora': 0x2, 'King Zora': 0x2,
'Sahasrahla': 0x10, 'Sahasrahla': 0x10,
@ -427,6 +430,9 @@ location_table_npc = {'Mushroom': 0x1000,
'Catfish': 0x20, 'Catfish': 0x20,
'Stumpy': 0x8, 'Stumpy': 0x8,
'Bombos Tablet': 0x200} 'Bombos Tablet': 0x200}
location_table_npc_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_npc.items()}
location_table_ow = {'Flute Spot': 0x2a, location_table_ow = {'Flute Spot': 0x2a,
'Sunken Treasure': 0x3b, 'Sunken Treasure': 0x3b,
"Zora's Ledge": 0x81, "Zora's Ledge": 0x81,
@ -439,11 +445,17 @@ location_table_ow = {'Flute Spot': 0x2a,
'Digging Game': 0x68, 'Digging Game': 0x68,
'Bumper Cave Ledge': 0x4a, 'Bumper Cave Ledge': 0x4a,
'Floating Island': 0x5} 'Floating Island': 0x5}
location_table_ow_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_ow.items()}
location_table_misc = {'Bottle Merchant': (0x3c9, 0x2), location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
'Purple Chest': (0x3c9, 0x10), 'Purple Chest': (0x3c9, 0x10),
"Link's Uncle": (0x3c6, 0x1), "Link's Uncle": (0x3c6, 0x1),
'Hobo': (0x3c9, 0x1)} 'Hobo': (0x3c9, 0x1)}
location_table_misc_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_misc.items()}
class SNESState(enum.IntEnum):
SNES_DISCONNECTED = 0 SNES_DISCONNECTED = 0
SNES_CONNECTING = 1 SNES_CONNECTING = 1
SNES_CONNECTED = 2 SNES_CONNECTED = 2
@ -518,15 +530,15 @@ async def get_snes_devices(ctx: Context):
async def snes_connect(ctx: Context, address): async def snes_connect(ctx: Context, address):
global SNES_RECONNECT_DELAY global SNES_RECONNECT_DELAY
if ctx.snes_socket is not None and ctx.snes_state == SNES_CONNECTED: if ctx.snes_socket is not None and ctx.snes_state == SNESState.SNES_CONNECTED:
logger.error('Already connected to snes') logger.error('Already connected to snes')
return return
recv_task = None recv_task = None
ctx.snes_state = SNES_CONNECTING ctx.snes_state = SNESState.SNES_CONNECTING
socket = await _snes_connect(ctx, address) socket = await _snes_connect(ctx, address)
ctx.snes_socket = socket ctx.snes_socket = socket
ctx.snes_state = SNES_CONNECTED ctx.snes_state = SNESState.SNES_CONNECTED
try: try:
devices = await get_snes_devices(ctx) devices = await get_snes_devices(ctx)
@ -552,7 +564,7 @@ async def snes_connect(ctx: Context, address):
"Operands": [device] "Operands": [device]
} }
await ctx.snes_socket.send(dumps(Attach_Request)) await ctx.snes_socket.send(dumps(Attach_Request))
ctx.snes_state = SNES_ATTACHED ctx.snes_state = SNESState.SNES_ATTACHED
ctx.snes_attached_device = (devices.index(device), device) ctx.snes_attached_device = (devices.index(device), device)
ctx.ui_node.send_connection_status(ctx) ctx.ui_node.send_connection_status(ctx)
@ -579,7 +591,7 @@ async def snes_connect(ctx: Context, address):
if not ctx.snes_socket.closed: if not ctx.snes_socket.closed:
await ctx.snes_socket.close() await ctx.snes_socket.close()
ctx.snes_socket = None ctx.snes_socket = None
ctx.snes_state = SNES_DISCONNECTED ctx.snes_state = SNESState.SNES_DISCONNECTED
if not ctx.snes_reconnect_address: if not ctx.snes_reconnect_address:
logger.error("Error connecting to snes (%s)" % e) logger.error("Error connecting to snes (%s)" % e)
else: else:
@ -620,7 +632,7 @@ async def snes_recv_loop(ctx: Context):
if socket is not None and not socket.closed: if socket is not None and not socket.closed:
await socket.close() await socket.close()
ctx.snes_state = SNES_DISCONNECTED ctx.snes_state = SNESState.SNES_DISCONNECTED
ctx.snes_recv_queue = asyncio.Queue() ctx.snes_recv_queue = asyncio.Queue()
ctx.hud_message_queue = [] ctx.hud_message_queue = []
ctx.ui_node.send_connection_status(ctx) ctx.ui_node.send_connection_status(ctx)
@ -636,7 +648,7 @@ async def snes_read(ctx: Context, address, size):
try: try:
await ctx.snes_request_lock.acquire() await ctx.snes_request_lock.acquire()
if ctx.snes_state != SNES_ATTACHED or ctx.snes_socket is None or not ctx.snes_socket.open or ctx.snes_socket.closed: if ctx.snes_state != SNESState.SNES_ATTACHED or ctx.snes_socket is None or not ctx.snes_socket.open or ctx.snes_socket.closed:
return None return None
GetAddress_Request = { GetAddress_Request = {
@ -675,7 +687,8 @@ async def snes_write(ctx: Context, write_list):
try: try:
await ctx.snes_request_lock.acquire() await ctx.snes_request_lock.acquire()
if ctx.snes_state != SNES_ATTACHED or ctx.snes_socket is None or not ctx.snes_socket.open or ctx.snes_socket.closed: if ctx.snes_state != SNESState.SNES_ATTACHED or ctx.snes_socket is None or \
not ctx.snes_socket.open or ctx.snes_socket.closed:
return False return False
PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'} PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
@ -897,7 +910,7 @@ async def process_server_cmd(ctx: Context, args: dict):
msgs = [] msgs = []
if ctx.locations_checked: if ctx.locations_checked:
msgs.append({"cmd": "LocationChecks", msgs.append({"cmd": "LocationChecks",
"locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]}) "locations": list(ctx.locations_checked)})
if ctx.locations_scouted: if ctx.locations_scouted:
msgs.append({"cmd": "LocationScouts", msgs.append({"cmd": "LocationScouts",
"locations": list(ctx.locations_scouted)}) "locations": list(ctx.locations_scouted)})
@ -910,8 +923,8 @@ async def process_server_cmd(ctx: Context, args: dict):
# This list is used to only send to the server what is reported as ACTUALLY Missing. # This list is used to only send to the server what is reported as ACTUALLY Missing.
# This also serves to allow an easy visual of what locations were already checked previously # This also serves to allow an easy visual of what locations were already checked previously
# when /missing is used for the client side view of what is missing. # when /missing is used for the client side view of what is missing.
ctx.items_missing = args["missing_checks"] ctx.missing_locations = args["missing_locations"]
ctx.items_checked = args["items_checked"] ctx.checked_locations = args["checked_locations"]
elif cmd == 'ReceivedItems': elif cmd == 'ReceivedItems':
start_index = args["index"] start_index = args["index"]
@ -922,7 +935,7 @@ async def process_server_cmd(ctx: Context, args: dict):
sync_msg = [{'cmd': 'Sync'}] sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked: if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", sync_msg.append({"cmd": "LocationChecks",
"locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]}) "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg) await ctx.send_msgs(sync_msg)
if start_index == len(ctx.items_received): if start_index == len(ctx.items_received):
for item in args['items']: for item in args['items']:
@ -1055,11 +1068,11 @@ class ClientCommandProcessor(CommandProcessor):
for location, location_id in Regions.lookup_name_to_id.items(): for location, location_id in Regions.lookup_name_to_id.items():
if location_id < 0: if location_id < 0:
continue continue
if location not in self.ctx.locations_checked: if location_id not in self.ctx.locations_checked:
if location in self.ctx.items_missing: if location_id in self.ctx.missing_locations:
self.output('Missing: ' + location) self.output('Missing: ' + location)
count += 1 count += 1
elif self.ctx.items_checked is None or location in self.ctx.items_checked: elif location_id in self.ctx.checked_locations:
self.output('Checked: ' + location) self.output('Checked: ' + location)
count += 1 count += 1
checked_count += 1 checked_count += 1
@ -1078,7 +1091,7 @@ class ClientCommandProcessor(CommandProcessor):
else: else:
self.ctx.slow_mode = not self.ctx.slow_mode self.ctx.slow_mode = not self.ctx.slow_mode
logger.info(f"Setting slow mode to {self.ctx.slow_mode}") self.output(f"Setting slow mode to {self.ctx.slow_mode}")
def _cmd_web(self): def _cmd_web(self):
if self.ctx.webui_socket_port: if self.ctx.webui_socket_port:
@ -1086,6 +1099,16 @@ class ClientCommandProcessor(CommandProcessor):
else: else:
self.output("Web UI was never started.") self.output("Web UI was never started.")
def _cmd_ready(self):
self.ctx.ready = not self.ctx.ready
if self.ctx.ready:
state = CLientStatus.CLIENT_READY
self.output("Readied up.")
else:
state = CLientStatus.CLIENT_CONNECTED
self.output("Unreadied.")
asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]))
def default(self, raw: str): def default(self, raw: str):
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}])) asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]))
@ -1114,38 +1137,29 @@ async def console_loop(ctx: Context):
async def track_locations(ctx: Context, roomid, roomdata): async def track_locations(ctx: Context, roomid, roomdata):
new_locations = [] new_locations = []
def new_check(location): def new_check(location_id):
new_locations.append(Regions.lookup_name_to_id.get(location, Shops.shop_table_by_location.get(location, -1))) new_locations.append(location_id)
ctx.locations_checked.add(location) ctx.locations_checked.add(location_id)
location = ctx.location_name_getter(location_id)
check = None logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
if ctx.items_checked is None:
check = f'New Check: {location} ({len(ctx.locations_checked)}/{len(Regions.lookup_name_to_id)})'
else:
items_total = len(ctx.items_missing) + len(ctx.items_checked)
if location in ctx.items_missing or location in ctx.items_checked:
ctx.locations_recognized.add(location)
check = f'New Check: {location} ({len(ctx.locations_recognized)}/{items_total})'
if check:
logger.info(check)
ctx.ui_node.send_location_check(ctx, location) ctx.ui_node.send_location_check(ctx, location)
try: try:
if roomid in location_shop_ids: if roomid in location_shop_ids:
misc_data = await snes_read(ctx, SHOP_ADDR, (len(location_shop_order) * 3) + 5) misc_data = await snes_read(ctx, SHOP_ADDR, (len(location_shop_order) * 3) + 5)
for cnt, b in enumerate(misc_data): for cnt, b in enumerate(misc_data):
my_check = Shops.shop_table_by_location_id[Shops.SHOP_ID_START + cnt] if int(b) and (Shops.SHOP_ID_START + cnt) not in ctx.locations_checked:
if int(b) > 0 and my_check not in ctx.locations_checked: new_check(Shops.SHOP_ID_START + cnt)
new_check(my_check)
except Exception as e: except Exception as e:
logger.info(f"Exception: {e}") logger.info(f"Exception: {e}")
for location, (loc_roomid, loc_mask) in location_table_uw.items(): for location_id, (loc_roomid, loc_mask) in location_table_uw_id.items():
try: try:
if location not in ctx.locations_checked and loc_roomid == roomid and (
if location_id not in ctx.locations_checked and loc_roomid == roomid and (
roomdata << 4) & loc_mask != 0: roomdata << 4) & loc_mask != 0:
new_check(location) new_check(location_id)
except Exception as e: except Exception as e:
logger.exception(f"Exception: {e}") logger.exception(f"Exception: {e}")
@ -1153,48 +1167,51 @@ async def track_locations(ctx: Context, roomid, roomdata):
ow_end = uw_end = 0 ow_end = uw_end = 0
uw_unchecked = {} uw_unchecked = {}
for location, (roomid, mask) in location_table_uw.items(): for location, (roomid, mask) in location_table_uw.items():
if location not in ctx.locations_checked: location_id = Regions.lookup_name_to_id[location]
uw_unchecked[location] = (roomid, mask) if location_id not in ctx.locations_checked:
uw_unchecked[location_id] = (roomid, mask)
uw_begin = min(uw_begin, roomid) uw_begin = min(uw_begin, roomid)
uw_end = max(uw_end, roomid + 1) uw_end = max(uw_end, roomid + 1)
if uw_begin < uw_end: if uw_begin < uw_end:
uw_data = await snes_read(ctx, SAVEDATA_START + (uw_begin * 2), (uw_end - uw_begin) * 2) uw_data = await snes_read(ctx, SAVEDATA_START + (uw_begin * 2), (uw_end - uw_begin) * 2)
if uw_data is not None: if uw_data is not None:
for location, (roomid, mask) in uw_unchecked.items(): for location_id, (roomid, mask) in uw_unchecked.items():
offset = (roomid - uw_begin) * 2 offset = (roomid - uw_begin) * 2
roomdata = uw_data[offset] | (uw_data[offset + 1] << 8) roomdata = uw_data[offset] | (uw_data[offset + 1] << 8)
if roomdata & mask != 0: if roomdata & mask != 0:
new_check(location) new_check(location_id)
ow_begin = 0x82 ow_begin = 0x82
ow_unchecked = {} ow_unchecked = {}
for location, screenid in location_table_ow.items(): for location_id, screenid in location_table_ow_id.items():
if location not in ctx.locations_checked: if location_id not in ctx.locations_checked:
ow_unchecked[location] = screenid ow_unchecked[location_id] = screenid
ow_begin = min(ow_begin, screenid) ow_begin = min(ow_begin, screenid)
ow_end = max(ow_end, screenid + 1) ow_end = max(ow_end, screenid + 1)
if ow_begin < ow_end: if ow_begin < ow_end:
ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin) ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin)
if ow_data is not None: if ow_data is not None:
for location, screenid in ow_unchecked.items(): for location_id, screenid in ow_unchecked.items():
if ow_data[screenid - ow_begin] & 0x40 != 0: if ow_data[screenid - ow_begin] & 0x40 != 0:
new_check(location) new_check(location_id)
if not all(location in ctx.locations_checked for location in location_table_npc.keys()): if not ctx.locations_checked.issuperset(location_table_npc_id):
npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2) npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
if npc_data is not None: if npc_data is not None:
npc_value = npc_data[0] | (npc_data[1] << 8) npc_value = npc_data[0] | (npc_data[1] << 8)
for location, mask in location_table_npc.items(): for location_id, mask in location_table_npc_id.items():
if npc_value & mask != 0 and location not in ctx.locations_checked: if npc_value & mask != 0 and location_id not in ctx.locations_checked:
new_check(location) new_check(location_id)
if not all(location in ctx.locations_checked for location in location_table_misc.keys()): if not ctx.locations_checked.issuperset(location_table_misc_id):
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4) misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
if misc_data is not None: if misc_data is not None:
for location, (offset, mask) in location_table_misc.items(): for location_id, (offset, mask) in location_table_misc_id.items():
assert (0x3c6 <= offset <= 0x3c9) assert (0x3c6 <= offset <= 0x3c9)
if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.locations_checked: if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked:
new_check(location) new_check(location_id)
if new_locations: if new_locations:
@ -1292,7 +1309,6 @@ async def game_watcher(ctx: Context):
if scout_location > 0 and scout_location not in ctx.locations_scouted: if scout_location > 0 and scout_location not in ctx.locations_scouted:
ctx.locations_scouted.add(scout_location) ctx.locations_scouted.add(scout_location)
logger.info(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}')
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}]) await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
await track_locations(ctx, roomid, roomdata) await track_locations(ctx, roomid, roomdata)

View File

@ -240,7 +240,7 @@ class Context(Node):
return d return d
def set_save(self, savedata: dict): def set_save(self, savedata: dict):
rom_names = savedata["rom_names"] # convert from TrackerList to List in case of ponyorm
received_items = {tuple(k): [NetworkItem(*i) for i in v] for k, v in savedata["received_items"]} received_items = {tuple(k): [NetworkItem(*i) for i in v] for k, v in savedata["received_items"]}
self.received_items = received_items self.received_items = received_items
@ -367,6 +367,7 @@ async def on_client_disconnected(ctx: Context, client: Client):
async def on_client_joined(ctx: Context, client: Client): async def on_client_joined(ctx: Context, client: Client):
update_client_status(ctx, client, CLientStatus.CLIENT_CONNECTED)
version_str = '.'.join(str(x) for x in client.version) version_str = '.'.join(str(x) for x in client.version)
ctx.notify_all( ctx.notify_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has joined the game. " f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has joined the game. "
@ -375,10 +376,9 @@ async def on_client_joined(ctx: Context, client: Client):
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
async def on_client_left(ctx: Context, client: Client): async def on_client_left(ctx: Context, client: Client):
update_client_status(ctx, client, CLientStatus.CLIENT_UNKNOWN)
ctx.notify_all("%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1)) ctx.notify_all("%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1))
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
if ctx.commandprocessor.client == Client:
ctx.commandprocessor.client = None
async def countdown(ctx: Context, timer): async def countdown(ctx: Context, timer):
@ -418,7 +418,7 @@ def get_received_items(ctx: Context, team: int, player: int) -> typing.List[Netw
def tuplize_received_items(items): def tuplize_received_items(items):
return [(item.item, item.location, item.player) for item in items] return [NetworkItem(item.item, item.location, item.player) for item in items]
def send_new_items(ctx: Context): def send_new_items(ctx: Context):
@ -791,7 +791,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
locations = get_missing_checks(self.ctx, self.client) locations = get_missing_checks(self.ctx, self.client)
if locations: if locations:
texts = [f'Missing: {location}\n' for location in locations] texts = [f'Missing: {get_item_name_from_id(location)}\n' for location in locations]
texts.append(f"Found {len(locations)} missing location checks") texts.append(f"Found {len(locations)} missing location checks")
self.ctx.notify_client_multiple(self.client, texts) self.ctx.notify_client_multiple(self.client, texts)
else: else:
@ -914,15 +914,15 @@ class ClientMessageProcessor(CommonCommandProcessor):
return False return False
def get_checked_checks(ctx: Context, client: Client) -> list: def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
return [Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}') for return [location_id for
location_id, slot in ctx.locations if location_id, slot in ctx.locations if
slot == client.slot and slot == client.slot and
location_id in ctx.location_checks[client.team, client.slot]] location_id in ctx.location_checks[client.team, client.slot]]
def get_missing_checks(ctx: Context, client: Client) -> list: def get_missing_checks(ctx: Context, client: Client) -> typing.List[int]:
return [Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}') for return [location_id for
location_id, slot in ctx.locations if location_id, slot in ctx.locations if
slot == client.slot and slot == client.slot and
location_id not in ctx.location_checks[client.team, client.slot]] location_id not in ctx.location_checks[client.team, client.slot]]
@ -1000,8 +1000,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
"cmd": "Connected", "cmd": "Connected",
"team": client.team, "slot": client.slot, "team": client.team, "slot": client.slot,
"players": ctx.get_players_package(), "players": ctx.get_players_package(),
"missing_checks": get_missing_checks(ctx, client), "missing_locations": get_missing_checks(ctx, client),
"items_checked": get_checked_checks(ctx, client)}] "checked_locations": get_checked_checks(ctx, client)}]
items = get_received_items(ctx, client.team, client.slot) items = get_received_items(ctx, client.team, client.slot)
if items: if items:
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)}) reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)})
@ -1044,15 +1044,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
elif cmd == 'StatusUpdate': elif cmd == 'StatusUpdate':
current = ctx.client_game_state[client.team, client.slot] update_client_status(ctx, client, args["status"])
if current != CLientStatus.CLIENT_GOAL: # can't undo goal completion
if args["status"] == CLientStatus.CLIENT_GOAL:
finished_msg = f'{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has completed their goal.'
ctx.notify_all(finished_msg)
if "auto" in ctx.forfeit_mode:
forfeit_player(ctx, client.team, client.slot)
ctx.client_game_state[client.team, client.slot] = args["status"]
if cmd == 'Say': if cmd == 'Say':
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable(): if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
@ -1061,6 +1053,16 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
client.messageprocessor(args["text"]) client.messageprocessor(args["text"])
def update_client_status(ctx: Context, client: Client, new_status: CLientStatus):
current = ctx.client_game_state[client.team, client.slot]
if current != CLientStatus.CLIENT_GOAL: # can't undo goal completion
if new_status == CLientStatus.CLIENT_GOAL:
finished_msg = f'{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has completed their goal.'
ctx.notify_all(finished_msg)
if "auto" in ctx.forfeit_mode:
forfeit_player(ctx, client.team, client.slot)
ctx.client_game_state[client.team, client.slot] = new_status
class ServerCommandProcessor(CommonCommandProcessor): class ServerCommandProcessor(CommonCommandProcessor):
def __init__(self, ctx: Context): def __init__(self, ctx: Context):

View File

@ -21,7 +21,7 @@ class JSONMessagePart(typing.TypedDict, total=False):
class CLientStatus(enum.IntEnum): class CLientStatus(enum.IntEnum):
CLIENT_UNKNOWN = 0 CLIENT_UNKNOWN = 0
# CLIENT_CONNECTED = 5 maybe? CLIENT_CONNECTED = 5
CLIENT_READY = 10 CLIENT_READY = 10
CLIENT_PLAYING = 20 CLIENT_PLAYING = 20
CLIENT_GOAL = 30 CLIENT_GOAL = 30

View File

@ -164,9 +164,7 @@ def ShopSlotFill(world):
blacklist_word in item_name for blacklist_word in blacklist_words)} blacklist_word in item_name for blacklist_word in blacklist_words)}
blacklist_words.add("Bee") blacklist_words.add("Bee")
locations_per_sphere = list(list(sphere).sort(key=lambda location: location.name) for sphere in world.get_spheres()) locations_per_sphere = list(sorted(sphere, key=lambda location: location.name) for sphere in world.get_spheres())
# currently special care needs to be taken so that Shop.region.locations.item is identical to Shop.inventory # currently special care needs to be taken so that Shop.region.locations.item is identical to Shop.inventory
# Potentially create Locations as needed and make inventory the only source, to prevent divergence # Potentially create Locations as needed and make inventory the only source, to prevent divergence

View File

@ -281,7 +281,6 @@ junk_texts = [
"{C:GREEN}\nTheres always\nmoney in the\nBanana Stand>", "{C:GREEN}\nTheres always\nmoney in the\nBanana Stand>",
"{C:GREEN}\n \nJust walk away\n >", "{C:GREEN}\n \nJust walk away\n >",
"{C:GREEN}\neverybody is\nlooking for\nsomething >", "{C:GREEN}\neverybody is\nlooking for\nsomething >",
"{C:GREEN}\nCandy Is Dandy\nBut liquor\nIs quicker. >",
"{C:GREEN}\nSpring Ball\nare behind\nRidley >", "{C:GREEN}\nSpring Ball\nare behind\nRidley >",
"{C:GREEN}\nThe gnome asks\nyou to guess\nhis name. >", "{C:GREEN}\nThe gnome asks\nyou to guess\nhis name. >",
"{C:GREEN}\nI heard beans\non toast is a\ngreat meal. >", "{C:GREEN}\nI heard beans\non toast is a\ngreat meal. >",