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:
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:
if location.can_reach(state):
sphere.append(location)
@ -433,8 +434,6 @@ class MultiWorld():
state.collect(location.item, True, location)
locations -= sphere
def fulfills_accessibility(self, state: Optional[CollectionState] = None):
"""Check if accessibility rules are fulfilled with current or supplied state."""
if not state:

View File

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

View File

@ -9,13 +9,12 @@ import socket
import os
import subprocess
import base64
import re
import shutil
from json import loads, dumps
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.")
@ -54,6 +53,7 @@ class Context():
# WebUI Stuff
self.ui_node = WebUI.WebUiClient()
logger.addHandler(self.ui_node)
self.ready = False
self.custom_address = None
self.webui_socket_port: typing.Optional[int] = port
self.hint_cost = 0
@ -70,7 +70,7 @@ class Context():
self.input_requests = 0
self.snes_socket = None
self.snes_state = SNES_DISCONNECTED
self.snes_state = SNESState.SNES_DISCONNECTED
self.snes_attached_device = None
self.snes_reconnect_address = None
self.snes_recv_queue = asyncio.Queue()
@ -87,12 +87,12 @@ class Context():
self.slot = None
self.player_names: typing.Dict[int: str] = {}
self.locations_recognized = set()
# these should probably track IDs where possible
self.locations_checked:typing.Set[str] = set()
self.locations_scouted:typing.Set[str] = set()
self.locations_checked:typing.Set[int] = set()
self.locations_scouted:typing.Set[int] = set()
self.items_received = []
self.items_missing = []
self.items_checked = None
self.missing_locations: typing.List[int] = []
self.checked_locations: typing.List[int] = []
self.locations_info = {}
self.awaiting_rom = False
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 - Pre-Moldorm Chest': (0x3d, 0x40),
'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,
'King Zora': 0x2,
'Sahasrahla': 0x10,
@ -427,6 +430,9 @@ location_table_npc = {'Mushroom': 0x1000,
'Catfish': 0x20,
'Stumpy': 0x8,
'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,
'Sunken Treasure': 0x3b,
"Zora's Ledge": 0x81,
@ -439,15 +445,21 @@ location_table_ow = {'Flute Spot': 0x2a,
'Digging Game': 0x68,
'Bumper Cave Ledge': 0x4a,
'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),
'Purple Chest': (0x3c9, 0x10),
"Link's Uncle": (0x3c6, 0x1),
'Hobo': (0x3c9, 0x1)}
SNES_DISCONNECTED = 0
SNES_CONNECTING = 1
SNES_CONNECTED = 2
SNES_ATTACHED = 3
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_CONNECTING = 1
SNES_CONNECTED = 2
SNES_ATTACHED = 3
def launch_qusb2snes(ctx: Context):
@ -518,15 +530,15 @@ async def get_snes_devices(ctx: Context):
async def snes_connect(ctx: Context, address):
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')
return
recv_task = None
ctx.snes_state = SNES_CONNECTING
ctx.snes_state = SNESState.SNES_CONNECTING
socket = await _snes_connect(ctx, address)
ctx.snes_socket = socket
ctx.snes_state = SNES_CONNECTED
ctx.snes_state = SNESState.SNES_CONNECTED
try:
devices = await get_snes_devices(ctx)
@ -552,7 +564,7 @@ async def snes_connect(ctx: Context, address):
"Operands": [device]
}
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.ui_node.send_connection_status(ctx)
@ -579,7 +591,7 @@ async def snes_connect(ctx: Context, address):
if not ctx.snes_socket.closed:
await ctx.snes_socket.close()
ctx.snes_socket = None
ctx.snes_state = SNES_DISCONNECTED
ctx.snes_state = SNESState.SNES_DISCONNECTED
if not ctx.snes_reconnect_address:
logger.error("Error connecting to snes (%s)" % e)
else:
@ -620,7 +632,7 @@ async def snes_recv_loop(ctx: Context):
if socket is not None and not socket.closed:
await socket.close()
ctx.snes_state = SNES_DISCONNECTED
ctx.snes_state = SNESState.SNES_DISCONNECTED
ctx.snes_recv_queue = asyncio.Queue()
ctx.hud_message_queue = []
ctx.ui_node.send_connection_status(ctx)
@ -636,7 +648,7 @@ async def snes_read(ctx: Context, address, size):
try:
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
GetAddress_Request = {
@ -675,7 +687,8 @@ async def snes_write(ctx: Context, write_list):
try:
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
PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
@ -897,7 +910,7 @@ async def process_server_cmd(ctx: Context, args: dict):
msgs = []
if ctx.locations_checked:
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:
msgs.append({"cmd": "LocationScouts",
"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 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.
ctx.items_missing = args["missing_checks"]
ctx.items_checked = args["items_checked"]
ctx.missing_locations = args["missing_locations"]
ctx.checked_locations = args["checked_locations"]
elif cmd == 'ReceivedItems':
start_index = args["index"]
@ -922,7 +935,7 @@ async def process_server_cmd(ctx: Context, args: dict):
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
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)
if start_index == len(ctx.items_received):
for item in args['items']:
@ -1055,11 +1068,11 @@ class ClientCommandProcessor(CommandProcessor):
for location, location_id in Regions.lookup_name_to_id.items():
if location_id < 0:
continue
if location not in self.ctx.locations_checked:
if location in self.ctx.items_missing:
if location_id not in self.ctx.locations_checked:
if location_id in self.ctx.missing_locations:
self.output('Missing: ' + location)
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)
count += 1
checked_count += 1
@ -1078,7 +1091,7 @@ class ClientCommandProcessor(CommandProcessor):
else:
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):
if self.ctx.webui_socket_port:
@ -1086,6 +1099,16 @@ class ClientCommandProcessor(CommandProcessor):
else:
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):
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):
new_locations = []
def new_check(location):
new_locations.append(Regions.lookup_name_to_id.get(location, Shops.shop_table_by_location.get(location, -1)))
ctx.locations_checked.add(location)
check = None
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)
def new_check(location_id):
new_locations.append(location_id)
ctx.locations_checked.add(location_id)
location = ctx.location_name_getter(location_id)
logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
ctx.ui_node.send_location_check(ctx, location)
try:
if roomid in location_shop_ids:
misc_data = await snes_read(ctx, SHOP_ADDR, (len(location_shop_order) * 3) + 5)
for cnt, b in enumerate(misc_data):
my_check = Shops.shop_table_by_location_id[Shops.SHOP_ID_START + cnt]
if int(b) > 0 and my_check not in ctx.locations_checked:
new_check(my_check)
if int(b) and (Shops.SHOP_ID_START + cnt) not in ctx.locations_checked:
new_check(Shops.SHOP_ID_START + cnt)
except Exception as 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:
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:
new_check(location)
new_check(location_id)
except Exception as e:
logger.exception(f"Exception: {e}")
@ -1153,48 +1167,51 @@ async def track_locations(ctx: Context, roomid, roomdata):
ow_end = uw_end = 0
uw_unchecked = {}
for location, (roomid, mask) in location_table_uw.items():
if location not in ctx.locations_checked:
uw_unchecked[location] = (roomid, mask)
location_id = Regions.lookup_name_to_id[location]
if location_id not in ctx.locations_checked:
uw_unchecked[location_id] = (roomid, mask)
uw_begin = min(uw_begin, roomid)
uw_end = max(uw_end, roomid + 1)
if uw_begin < uw_end:
uw_data = await snes_read(ctx, SAVEDATA_START + (uw_begin * 2), (uw_end - uw_begin) * 2)
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
roomdata = uw_data[offset] | (uw_data[offset + 1] << 8)
if roomdata & mask != 0:
new_check(location)
new_check(location_id)
ow_begin = 0x82
ow_unchecked = {}
for location, screenid in location_table_ow.items():
if location not in ctx.locations_checked:
ow_unchecked[location] = screenid
for location_id, screenid in location_table_ow_id.items():
if location_id not in ctx.locations_checked:
ow_unchecked[location_id] = screenid
ow_begin = min(ow_begin, screenid)
ow_end = max(ow_end, screenid + 1)
if ow_begin < ow_end:
ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin)
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:
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)
if npc_data is not None:
npc_value = npc_data[0] | (npc_data[1] << 8)
for location, mask in location_table_npc.items():
if npc_value & mask != 0 and location not in ctx.locations_checked:
new_check(location)
for location_id, mask in location_table_npc_id.items():
if npc_value & mask != 0 and location_id not in ctx.locations_checked:
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)
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)
if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.locations_checked:
new_check(location)
if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked:
new_check(location_id)
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:
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 track_locations(ctx, roomid, roomdata)

View File

@ -240,7 +240,7 @@ class Context(Node):
return d
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"]}
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):
update_client_status(ctx, client, CLientStatus.CLIENT_CONNECTED)
version_str = '.'.join(str(x) for x in client.version)
ctx.notify_all(
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)
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.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):
@ -418,7 +418,7 @@ def get_received_items(ctx: Context, team: int, player: int) -> typing.List[Netw
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):
@ -791,7 +791,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
locations = get_missing_checks(self.ctx, self.client)
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")
self.ctx.notify_client_multiple(self.client, texts)
else:
@ -914,15 +914,15 @@ class ClientMessageProcessor(CommonCommandProcessor):
return False
def get_checked_checks(ctx: Context, client: Client) -> list:
return [Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}') for
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
return [location_id for
location_id, slot in ctx.locations if
slot == client.slot and
location_id in ctx.location_checks[client.team, client.slot]]
def get_missing_checks(ctx: Context, client: Client) -> list:
return [Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}') for
def get_missing_checks(ctx: Context, client: Client) -> typing.List[int]:
return [location_id for
location_id, slot in ctx.locations if
slot == client.slot and
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",
"team": client.team, "slot": client.slot,
"players": ctx.get_players_package(),
"missing_checks": get_missing_checks(ctx, client),
"items_checked": get_checked_checks(ctx, client)}]
"missing_locations": get_missing_checks(ctx, client),
"checked_locations": get_checked_checks(ctx, client)}]
items = get_received_items(ctx, client.team, client.slot)
if 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}])
elif cmd == 'StatusUpdate':
current = ctx.client_game_state[client.team, client.slot]
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"]
update_client_status(ctx, client, args["status"])
if cmd == 'Say':
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"])
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):
def __init__(self, ctx: Context):

View File

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

View File

@ -164,9 +164,7 @@ def ShopSlotFill(world):
blacklist_word in item_name for blacklist_word in blacklist_words)}
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
# 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}\n \nJust walk away\n >",
"{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}\nThe gnome asks\nyou to guess\nhis name. >",
"{C:GREEN}\nI heard beans\non toast is a\ngreat meal. >",