Skip to content

Commit

Permalink
Add per-client remote_item settings + TextOnly Tag
Browse files Browse the repository at this point in the history
* Tracker tag will receive all items via server (including local)
* TextOnly tag will receive no items
* TextClient sends TextOnly tag
* precollected items / start_inventory does not get an "Order received" number anymore
* local items do always get an "Order received" number now
* multisave changed, includes version number now, upgrade works for games (not trackers)
  • Loading branch information
black-sliver authored and Berserker66 committed Jan 21, 2022
1 parent 344f4af commit 0c46cc6
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 30 deletions.
2 changes: 1 addition & 1 deletion CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ def get_base_parser(description=None):
# Text Mode to use !hint and such with games that have no text entry

class TextContext(CommonContext):
tags = {"AP", "IgnoreGame"}
tags = {"AP", "IgnoreGame", "TextOnly"}
game = "Archipelago"

async def server_auth(self, password_requested: bool = False):
Expand Down
100 changes: 76 additions & 24 deletions MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
class Client(Endpoint):
version = Version(0, 0, 0)
tags: typing.List[str] = []
remote_items: bool
remote_start_inventory: bool
no_items: bool
no_locations: bool

def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
super().__init__(socket)
Expand All @@ -52,6 +56,12 @@ def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
self.messageprocessor = client_message_processor(ctx, self)
self.ctx = weakref.ref(ctx)

def parse_tags(self, ctx: Context):
self.remote_items = 'Tracker' in self.tags or self.slot in ctx.remote_items
self.remote_start_inventory = 'Tracker' in self.tags or self.slot in ctx.remote_start_inventory
self.no_items = 'TextOnly' in self.tags
self.no_locations = 'TextOnly' in self.tags or 'Tracker' in self.tags

@property
def name(self) -> str:
ctx = self.ctx()
Expand Down Expand Up @@ -79,6 +89,7 @@ class Context:
# team -> slot id -> list of clients authenticated to slot.
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
save_version = 2

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", collect_mode="disabled",
Expand Down Expand Up @@ -108,6 +119,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo
self.server = None
self.countdown_timer = 0
self.received_items = {}
self.start_inventory = {}
self.name_aliases: typing.Dict[team_slot, str] = {}
self.location_checks = collections.defaultdict(set)
self.hint_cost = hint_cost
Expand Down Expand Up @@ -273,11 +285,10 @@ def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
for player, loc_data in decoded_obj["er_hint_data"].items()}
self.games = decoded_obj["games"]
# award remote-items start inventory:
# load start inventory:
for slot, item_codes in decoded_obj["precollected_items"].items():
self.start_inventory[slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
for team in range(len(decoded_obj['names'])):
for slot, item_codes in decoded_obj["precollected_items"].items():
if slot in self.remote_start_inventory:
self.received_items[team, slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
for slot, hints in decoded_obj["precollected_hints"].items():
self.hints[team, slot].update(hints)
# declare slots without checks as done, as they're assumed to be spectators
Expand Down Expand Up @@ -351,6 +362,7 @@ def save_regularly():
def get_save(self) -> dict:
self.recheck_hints()
d = {
"version": self.save_version,
"connect_names": self.connect_names,
"received_items": self.received_items,
"hints_used": dict(self.hints_used),
Expand All @@ -370,7 +382,22 @@ def get_save(self) -> dict:
def set_save(self, savedata: dict):
if self.connect_names != savedata["connect_names"]:
raise Exception("This savegame does not appear to match the loaded multiworld.")
self.received_items = savedata["received_items"]
if "version" not in savedata:
# upgrade from version 1
# this is not perfect but good enough for old games to continue
for old, items in savedata["received_items"].items():
self.received_items[(*old, True)] = items
self.received_items[(*old, False)] = items.copy()
for (team, slot, remote) in self.received_items:
# remove start inventory from items, since this is separate now
start_inventory = get_start_inventory(self, team, slot, slot in self.remote_start_inventory)
if start_inventory:
del self.received_items[team, slot, remote][:len(start_inventory)]
logging.info("Upgraded save data")
elif savedata["version"] > self.save_version:
raise Exception("This savegame is newer than the server.")
else:
self.received_items = savedata["received_items"]
self.hints_used.update(savedata["hints_used"])
self.hints.update(savedata["hints"])

Expand Down Expand Up @@ -602,21 +629,29 @@ def get_status_string(ctx: Context, team: int):
return text


def get_received_items(ctx: Context, team: int, player: int) -> typing.List[NetworkItem]:
return ctx.received_items.setdefault((team, player), [])
def get_received_items(ctx: Context, team: int, player: int, remote_items: bool) -> typing.List[NetworkItem]:
return ctx.received_items.setdefault((team, player, remote_items), [])


def get_start_inventory(ctx: Context, team: int, player: int, remote_start_inventory: bool) -> typing.List[NetworkItem]:
return ctx.start_inventory.setdefault(player, []) if remote_start_inventory else []


def send_new_items(ctx: Context):
for team, clients in ctx.clients.items():
for slot, clients in clients.items():
items = get_received_items(ctx, team, slot)
for client in clients:
if len(items) > client.send_index:
if client.no_items:
continue
start_inventory = get_start_inventory(ctx, team, slot, client.remote_start_inventory)
items = get_received_items(ctx, team, slot, client.remote_items)
if len(start_inventory) + len(items) > client.send_index:
first_new_item = max(0, client.send_index - len(start_inventory))
asyncio.create_task(ctx.send_msgs(client, [{
"cmd": "ReceivedItems",
"index": client.send_index,
"items": items[client.send_index:]}]))
client.send_index = len(items)
"items": start_inventory[client.send_index:] + items[first_new_item:]}]))
client.send_index = len(start_inventory) + len(items)


def update_checked_locations(ctx: Context, team: int, slot: int):
Expand Down Expand Up @@ -670,8 +705,9 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
flags = 0

new_item = NetworkItem(item_id, location, slot, flags)
if target_player != slot or slot in ctx.remote_items:
get_received_items(ctx, team, target_player).append(new_item)
if target_player != slot:
get_received_items(ctx, team, target_player, False).append(new_item)
get_received_items(ctx, team, target_player, True).append(new_item)

logging.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
Expand Down Expand Up @@ -1098,7 +1134,8 @@ def _cmd_getitem(self, item_name: str) -> bool:
world.item_names)
if usable:
new_item = NetworkItem(world.create_item(item_name).code, -1, self.client.slot)
get_received_items(self.ctx, self.client.team, self.client.slot).append(new_item)
get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item)
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
self.ctx.notify_all(
'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team,
self.client.slot))
Expand Down Expand Up @@ -1290,6 +1327,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
ctx.clients[team][slot].append(client)
client.version = args['version']
client.tags = args['tags']
client.parse_tags(ctx)
reply = [{
"cmd": "Connected",
"team": client.team, "slot": client.slot,
Expand All @@ -1298,10 +1336,11 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
"checked_locations": get_checked_checks(ctx, team, slot),
"slot_data": ctx.slot_data[client.slot]
}]
items = get_received_items(ctx, client.team, client.slot)
if items:
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": items})
client.send_index = len(items)
start_inventory = get_start_inventory(ctx, team, slot, client.remote_start_inventory)
items = get_received_items(ctx, client.team, client.slot, client.remote_items)
if start_inventory or items:
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": start_inventory + items})
client.send_index = len(start_inventory) + len(items)
if not client.auth: # if this was a Re-Connect, don't print to console
client.auth = True
await on_client_joined(ctx, client)
Expand Down Expand Up @@ -1332,20 +1371,32 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
if "tags" in args:
old_tags = client.tags
client.tags = args["tags"]
client.parse_tags(ctx)
if "Tracker" in old_tags != "Tracker" in client.tags \
or "TextOnly" in old_tags != "TextOnly" in client.tags:
start_inventory = get_start_inventory(ctx, client.team, client.slot, client.remote_start_inventory)
items = get_received_items(ctx, client.team, client.slot, client.remote_items)
if start_inventory or items:
client.send_index = len(start_inventory) + len(items)
await ctx.send_msgs(client, [{"cmd": "ReceivedItems", "index": 0,
"items": start_inventory + items}])
else:
client.send_index = 0
if set(old_tags) != set(client.tags):
ctx.notify_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
f"from {old_tags} to {client.tags}.")

elif cmd == 'Sync':
items = get_received_items(ctx, client.team, client.slot)
if items:
client.send_index = len(items)
start_inventory = get_start_inventory(ctx, client.team, client.slot, client.remote_start_inventory)
items = get_received_items(ctx, client.team, client.slot, client.remote_items)
if start_inventory or items:
client.send_index = len(start_inventory) + len(items)
await ctx.send_msgs(client, [{"cmd": "ReceivedItems", "index": 0,
"items": items}])
"items": start_inventory + items}])

elif cmd == 'LocationChecks':
if "Tracker" in client.tags:
if client.no_locations:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
"text": "Trackers can't register new Location Checks",
"original_cmd": cmd}])
Expand Down Expand Up @@ -1527,7 +1578,8 @@ def _cmd_send(self, player_name: str, *item_name: str) -> bool:
item, usable, response = get_intended_text(item, world.item_names)
if usable:
new_item = NetworkItem(world.item_name_to_id[item], -1, 0)
get_received_items(self.ctx, team, slot).append(new_item)
get_received_items(self.ctx, team, slot, True).append(new_item)
get_received_items(self.ctx, team, slot, False).append(new_item)
self.ctx.notify_all('Cheat console: sending "' + item + '" to ' +
self.ctx.get_aliased_name(team, slot))
send_new_items(self.ctx)
Expand Down
10 changes: 6 additions & 4 deletions WebHostLib/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,11 +906,13 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic

checked_locations = multisave.get("location_checks", {}).get((team, player), set())
player_received_items = {}
if multisave.get('version', 0) > 0:
# add numbering to all items but starter_inventory
ordered_items = multisave.get('received_items', {}).get((team, player, True), [])
else:
ordered_items = multisave.get('received_items', {}).get((team, player), [])

for order_index, networkItem in enumerate(
multisave.get('received_items', {}).get((team, player), []),
start=1
):
for order_index, networkItem in enumerate(ordered_items, start=1):
player_received_items[networkItem.item] = order_index

return render_template("genericTracker.html",
Expand Down
3 changes: 2 additions & 1 deletion docs/network protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,8 @@ Tags are represented as a list of strings, the common Client tags follow:
| 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 and will refuse new locations from this client. |
| 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. |

### 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:
Expand Down

0 comments on commit 0c46cc6

Please sign in to comment.