Protocol: Improve machine-readability of prints (#1388)
* Protocol: Improve machine-readability of prints * Factorio: Make use of new PrintJSON fields for echo detection. * Protocol: Add message field to chat prints.
This commit is contained in:
parent
9e3c2e2464
commit
cc61f16e57
|
@ -341,6 +341,11 @@ class CommonContext:
|
||||||
return self.slot in self.slot_info[slot].group_members
|
return self.slot in self.slot_info[slot].group_members
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def is_echoed_chat(self, print_json_packet: dict) -> bool:
|
||||||
|
return print_json_packet.get("type", "") == "Chat" \
|
||||||
|
and print_json_packet.get("team", None) == self.team \
|
||||||
|
and print_json_packet.get("slot", None) == self.slot
|
||||||
|
|
||||||
def is_uninteresting_item_send(self, print_json_packet: dict) -> bool:
|
def is_uninteresting_item_send(self, print_json_packet: dict) -> bool:
|
||||||
"""Helper function for filtering out ItemSend prints that do not concern the local player."""
|
"""Helper function for filtering out ItemSend prints that do not concern the local player."""
|
||||||
return print_json_packet.get("type", "") == "ItemSend" \
|
return print_json_packet.get("type", "") == "ItemSend" \
|
||||||
|
|
|
@ -109,9 +109,10 @@ class FactorioContext(CommonContext):
|
||||||
|
|
||||||
def on_print_json(self, args: dict):
|
def on_print_json(self, args: dict):
|
||||||
if self.rcon_client:
|
if self.rcon_client:
|
||||||
if not self.filter_item_sends or not self.is_uninteresting_item_send(args):
|
if (not self.filter_item_sends or not self.is_uninteresting_item_send(args)) \
|
||||||
|
and not self.is_echoed_chat(args):
|
||||||
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
|
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
|
||||||
if not text.startswith(self.player_names[self.slot] + ":"):
|
if not text.startswith(self.player_names[self.slot] + ":"): # TODO: Remove string heuristic in the future.
|
||||||
self.print_to_game(text)
|
self.print_to_game(text)
|
||||||
super(FactorioContext, self).on_print_json(args)
|
super(FactorioContext, self).on_print_json(args)
|
||||||
|
|
||||||
|
|
|
@ -329,18 +329,18 @@ class Context:
|
||||||
self.clients[endpoint.team][endpoint.slot].remove(endpoint)
|
self.clients[endpoint.team][endpoint.slot].remove(endpoint)
|
||||||
await on_client_disconnected(self, endpoint)
|
await on_client_disconnected(self, endpoint)
|
||||||
|
|
||||||
|
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
|
||||||
def notify_client(self, client: Client, text: str):
|
|
||||||
if not client.auth:
|
if not client.auth:
|
||||||
return
|
return
|
||||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||||
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }]}]))
|
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
|
||||||
|
|
||||||
|
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
|
||||||
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
|
|
||||||
if not client.auth:
|
if not client.auth:
|
||||||
return
|
return
|
||||||
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }]} for text in texts]))
|
async_start(self.send_msgs(client,
|
||||||
|
[{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}
|
||||||
|
for text in texts]))
|
||||||
|
|
||||||
# loading
|
# loading
|
||||||
|
|
||||||
|
@ -666,7 +666,7 @@ class Context:
|
||||||
def on_goal_achieved(self, client: Client):
|
def on_goal_achieved(self, client: Client):
|
||||||
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
|
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
|
||||||
f' has completed their goal.'
|
f' has completed their goal.'
|
||||||
self.broadcast_text_all(finished_msg)
|
self.broadcast_text_all(finished_msg, {"type": "Goal", "team": client.team, "slot": client.slot})
|
||||||
if "auto" in self.collect_mode:
|
if "auto" in self.collect_mode:
|
||||||
collect_player(self, client.team, client.slot)
|
collect_player(self, client.team, client.slot)
|
||||||
if "auto" in self.release_mode:
|
if "auto" in self.release_mode:
|
||||||
|
@ -762,18 +762,21 @@ async def on_client_joined(ctx: Context, client: Client):
|
||||||
ctx.broadcast_text_all(
|
ctx.broadcast_text_all(
|
||||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
||||||
f"{verb} {ctx.games[client.slot]} has joined. "
|
f"{verb} {ctx.games[client.slot]} has joined. "
|
||||||
f"Client({version_str}), {client.tags}).")
|
f"Client({version_str}), {client.tags}).",
|
||||||
|
{"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags})
|
||||||
ctx.notify_client(client, "Now that you are connected, "
|
ctx.notify_client(client, "Now that you are connected, "
|
||||||
"you can use !help to list commands to run via the server. "
|
"you can use !help to list commands to run via the server. "
|
||||||
"If your client supports it, "
|
"If your client supports it, "
|
||||||
"you may have additional local commands you can list with /help.")
|
"you may have additional local commands you can list with /help.",
|
||||||
|
{"type": "Tutorial"})
|
||||||
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)
|
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
|
||||||
ctx.broadcast_text_all(
|
ctx.broadcast_text_all(
|
||||||
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1))
|
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1),
|
||||||
|
{"type": "Part", "team": client.team, "slot": client.slot})
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -859,9 +862,9 @@ def update_checked_locations(ctx: Context, team: int, slot: int):
|
||||||
def release_player(ctx: Context, team: int, slot: int):
|
def release_player(ctx: Context, team: int, slot: int):
|
||||||
"""register any locations that are in the multidata"""
|
"""register any locations that are in the multidata"""
|
||||||
all_locations = set(ctx.locations[slot])
|
all_locations = set(ctx.locations[slot])
|
||||||
ctx.broadcast_text_all(
|
ctx.broadcast_text_all("%s (Team #%d) has released all remaining items from their world."
|
||||||
"%s (Team #%d) has released all remaining items from their world." %
|
% (ctx.player_names[(team, slot)], team + 1),
|
||||||
(ctx.player_names[(team, slot)], team + 1))
|
{"type": "Release", "team": team, "slot": slot})
|
||||||
register_location_checks(ctx, team, slot, all_locations)
|
register_location_checks(ctx, team, slot, all_locations)
|
||||||
update_checked_locations(ctx, team, slot)
|
update_checked_locations(ctx, team, slot)
|
||||||
|
|
||||||
|
@ -874,8 +877,9 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
|
||||||
if values[1] == slot:
|
if values[1] == slot:
|
||||||
all_locations[source_slot].add(location_id)
|
all_locations[source_slot].add(location_id)
|
||||||
|
|
||||||
ctx.broadcast_text_all(
|
ctx.broadcast_text_all("%s (Team #%d) has collected their items from other worlds."
|
||||||
"%s (Team #%d) has collected their items from other worlds." % (ctx.player_names[(team, slot)], team + 1))
|
% (ctx.player_names[(team, slot)], team + 1),
|
||||||
|
{"type": "Collect", "team": team, "slot": slot})
|
||||||
for source_player, location_ids in all_locations.items():
|
for source_player, location_ids in all_locations.items():
|
||||||
register_location_checks(ctx, team, source_player, location_ids, count_activity=False)
|
register_location_checks(ctx, team, source_player, location_ids, count_activity=False)
|
||||||
update_checked_locations(ctx, team, source_player)
|
update_checked_locations(ctx, team, source_player)
|
||||||
|
@ -1145,11 +1149,15 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
|
|
||||||
def __call__(self, raw: str) -> typing.Optional[bool]:
|
def __call__(self, raw: str) -> typing.Optional[bool]:
|
||||||
if not raw.startswith("!admin"):
|
if not raw.startswith("!admin"):
|
||||||
self.ctx.broadcast_text_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + raw)
|
self.ctx.broadcast_text_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + raw,
|
||||||
|
{"type": "Chat", "team": self.client.team, "slot": self.client.slot, "message": raw})
|
||||||
return super(ClientMessageProcessor, self).__call__(raw)
|
return super(ClientMessageProcessor, self).__call__(raw)
|
||||||
|
|
||||||
def output(self, text):
|
def output(self, text: str):
|
||||||
self.ctx.notify_client(self.client, text)
|
self.ctx.notify_client(self.client, text, {"type": "CommandResult"})
|
||||||
|
|
||||||
|
def output_multiple(self, texts: typing.List[str]):
|
||||||
|
self.ctx.notify_client_multiple(self.client, texts, {"type": "CommandResult"})
|
||||||
|
|
||||||
def default(self, raw: str):
|
def default(self, raw: str):
|
||||||
pass # default is client sending just text
|
pass # default is client sending just text
|
||||||
|
@ -1172,9 +1180,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
# disallow others from knowing what the new remote administration password is.
|
# disallow others from knowing what the new remote administration password is.
|
||||||
"!admin /option server_password"):
|
"!admin /option server_password"):
|
||||||
output = f"!admin /option server_password {('*' * random.randint(4, 16))}"
|
output = f"!admin /option server_password {('*' * random.randint(4, 16))}"
|
||||||
# Otherwise notify the others what is happening.
|
self.ctx.broadcast_text_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + output,
|
||||||
self.ctx.broadcast_text_all(
|
{"type": "Chat", "team": self.client.team, "slot": self.client.slot, "message": output})
|
||||||
self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + output)
|
|
||||||
|
|
||||||
if not self.ctx.server_password:
|
if not self.ctx.server_password:
|
||||||
self.output("Sorry, Remote administration is disabled")
|
self.output("Sorry, Remote administration is disabled")
|
||||||
|
@ -1211,7 +1218,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
def _cmd_players(self) -> bool:
|
def _cmd_players(self) -> bool:
|
||||||
"""Get information about connected and missing players."""
|
"""Get information about connected and missing players."""
|
||||||
if len(self.ctx.player_names) < 10:
|
if len(self.ctx.player_names) < 10:
|
||||||
self.ctx.broadcast_text_all(get_players_string(self.ctx))
|
self.ctx.broadcast_text_all(get_players_string(self.ctx), {"type": "CommandResult"})
|
||||||
else:
|
else:
|
||||||
self.output(get_players_string(self.ctx))
|
self.output(get_players_string(self.ctx))
|
||||||
return True
|
return True
|
||||||
|
@ -1300,7 +1307,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
if locations:
|
if locations:
|
||||||
texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations]
|
texts = [f'Missing: {self.ctx.location_names[location]}' 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.output_multiple(texts)
|
||||||
else:
|
else:
|
||||||
self.output("No missing location checks found.")
|
self.output("No missing location checks found.")
|
||||||
return True
|
return True
|
||||||
|
@ -1313,7 +1320,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
if locations:
|
if locations:
|
||||||
texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations]
|
texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations]
|
||||||
texts.append(f"Found {len(locations)} done location checks")
|
texts.append(f"Found {len(locations)} done location checks")
|
||||||
self.ctx.notify_client_multiple(self.client, texts)
|
self.output_multiple(texts)
|
||||||
else:
|
else:
|
||||||
self.output("No done location checks found.")
|
self.output("No done location checks found.")
|
||||||
return True
|
return True
|
||||||
|
@ -1351,7 +1358,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
|
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
|
||||||
self.ctx.broadcast_text_all(
|
self.ctx.broadcast_text_all(
|
||||||
'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team,
|
'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team,
|
||||||
self.client.slot))
|
self.client.slot),
|
||||||
|
{"type": "ItemCheat", "team": self.client.team, "receiving": self.client.slot, "item": new_item})
|
||||||
send_new_items(self.ctx)
|
send_new_items(self.ctx)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
@ -1652,7 +1660,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||||
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
|
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
|
||||||
ctx.broadcast_text_all(
|
ctx.broadcast_text_all(
|
||||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
|
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
|
||||||
f"from {old_tags} to {client.tags}.")
|
f"from {old_tags} to {client.tags}.",
|
||||||
|
{"type": "TagsChanged", "team": client.team, "slot": client.slot, "tags": client.tags})
|
||||||
|
|
||||||
elif cmd == 'Sync':
|
elif cmd == 'Sync':
|
||||||
start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory)
|
start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory)
|
||||||
|
@ -1770,11 +1779,11 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
|
|
||||||
def output(self, text: str):
|
def output(self, text: str):
|
||||||
if self.client:
|
if self.client:
|
||||||
self.ctx.notify_client(self.client, text)
|
self.ctx.notify_client(self.client, text, {"type": "AdminCommandResult"})
|
||||||
super(ServerCommandProcessor, self).output(text)
|
super(ServerCommandProcessor, self).output(text)
|
||||||
|
|
||||||
def default(self, raw: str):
|
def default(self, raw: str):
|
||||||
self.ctx.broadcast_text_all('[Server]: ' + raw)
|
self.ctx.broadcast_text_all('[Server]: ' + raw, {"type": "ServerChat", "message": raw})
|
||||||
|
|
||||||
def _cmd_save(self) -> bool:
|
def _cmd_save(self) -> bool:
|
||||||
"""Save current state to multidata"""
|
"""Save current state to multidata"""
|
||||||
|
|
|
@ -9,7 +9,7 @@ These steps should be followed in order to establish a gameplay connection with
|
||||||
5. Client sends [Connect](#Connect) packet in order to authenticate with the server.
|
5. Client sends [Connect](#Connect) packet in order to authenticate with the server.
|
||||||
6. Server validates the client's packet and responds with [Connected](#Connected) or [ConnectionRefused](#ConnectionRefused).
|
6. Server validates the client's packet and responds with [Connected](#Connected) or [ConnectionRefused](#ConnectionRefused).
|
||||||
7. Server may send [ReceivedItems](#ReceivedItems) to the client, in the case that the client is missing items that are queued up for it.
|
7. Server may send [ReceivedItems](#ReceivedItems) to the client, in the case that the client is missing items that are queued up for it.
|
||||||
8. Server sends [PrintJSON](#PrintJSON) to all players to notify them of updates.
|
8. Server sends [PrintJSON](#PrintJSON) to all players to notify them of the new client connection.
|
||||||
|
|
||||||
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet.
|
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet.
|
||||||
|
|
||||||
|
@ -160,26 +160,43 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring:
|
||||||
All arguments for this packet are optional, only changes are sent.
|
All arguments for this packet are optional, only changes are sent.
|
||||||
|
|
||||||
### PrintJSON
|
### PrintJSON
|
||||||
Sent to clients purely to display a message to the player. The data being sent with this packet allows for configurability or specific messaging.
|
Sent to clients purely to display a message to the player. While various message types provide additional arguments, clients only need to evaluate the `data` argument to construct the human-readable message text. All other arguments may be ignored safely.
|
||||||
#### Arguments
|
#### Arguments
|
||||||
| Name | Type | Notes |
|
| Name | Type | Message Types | Contents |
|
||||||
| ---- | ---- | ----- |
|
| ---- | ---- | ------------- | -------- |
|
||||||
| data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. |
|
| data | list\[[JSONMessagePart](#JSONMessagePart)\] | (all) | Textual content of this message |
|
||||||
| type | str | May be present to indicate the [PrintJsonType](#PrintJsonType) of this message. |
|
| type | str | (any) | [PrintJsonType](#PrintJsonType) of this message (optional) |
|
||||||
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. |
|
| receiving | int | ItemSend, ItemCheat, Hint | Destination player's ID |
|
||||||
| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. |
|
| item | [NetworkItem](#NetworkItem) | ItemSend, ItemCheat, Hint | Source player's ID, location ID, item ID and item flags |
|
||||||
| found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. |
|
| found | bool | Hint | Whether the location hinted for was checked |
|
||||||
| countdown | int | Is present if type is `Countdown`, denotes the amount of seconds remaining on the countdown. |
|
| team | int | Join, Part, Chat, TagsChanged, Goal, Release, Collect, ItemCheat | Team of the triggering player |
|
||||||
|
| slot | int | Join, Part, Chat, TagsChanged, Goal, Release, Collect | Slot of the triggering player |
|
||||||
|
| message | str | Chat, ServerChat | Original chat message without sender prefix |
|
||||||
|
| tags | list\[str\] | Join, TagsChanged | Tags of the triggering player |
|
||||||
|
| countdown | int | Countdown | Amount of seconds remaining on the countdown |
|
||||||
|
|
||||||
##### PrintJsonType
|
#### PrintJsonType
|
||||||
PrintJsonType indicates the type of [PrintJson](#PrintJson) packet, different types can be handled differently by the client and can also contain additional arguments. When receiving an unknown or missing type the data's list\[[JSONMessagePart](#JSONMessagePart)\] should still be displayed to the player as normal text.
|
PrintJsonType indicates the type of a [PrintJSON](#PrintJSON) packet. Different types can be handled differently by the client and can also contain additional arguments. When receiving an unknown or missing type, the `data`'s list\[[JSONMessagePart](#JSONMessagePart)\] should still be displayed to the player as normal text.
|
||||||
|
|
||||||
Currently defined types are:
|
Currently defined types are:
|
||||||
| Type | Notes |
|
|
||||||
| ---- | ----- |
|
| Type | Subject |
|
||||||
| ItemSend | The message is in response to a player receiving an item. |
|
| ---- | ------- |
|
||||||
| Hint | The message is in response to a player hinting. |
|
| ItemSend | A player received an item. |
|
||||||
| Countdown | The message contains information about the current server Countdown. |
|
| ItemCheat | A player used the `!getitem` command. |
|
||||||
|
| Hint | A player hinted. |
|
||||||
|
| Join | A player connected. |
|
||||||
|
| Part | A player disconnected. |
|
||||||
|
| Chat | A player sent a chat message. |
|
||||||
|
| ServerChat | The server broadcasted a message. |
|
||||||
|
| Tutorial | The client has triggered a tutorial message, such as when first connecting. |
|
||||||
|
| TagsChanged | A player changed their tags. |
|
||||||
|
| CommandResult | Someone (usually the client) entered an `!` command. |
|
||||||
|
| AdminCommandResult | The client entered an `!admin` command. |
|
||||||
|
| Goal | A player reached their goal. |
|
||||||
|
| Release | A player released the remaining items in their world. |
|
||||||
|
| Collect | A player collected the remaining items for their world. |
|
||||||
|
| Countdown | The current server countdown has progressed. |
|
||||||
|
|
||||||
### DataPackage
|
### DataPackage
|
||||||
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info.
|
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info.
|
||||||
|
|
Loading…
Reference in New Issue