From cfff12d8d7a7ae36cef355d0f93449dd2d8b1027 Mon Sep 17 00:00:00 2001
From: recklesscoder <57289227+recklesscoder@users.noreply.github.com>
Date: Fri, 28 Oct 2022 00:45:26 +0200
Subject: [PATCH] Factorio: Added ability to chat from within the game. (#1068)

* Factorio: Added ability to chat from within the game.
This also allows using commands such as !hint from within the game.

* Factorio: Only prepend player names to chat in multiplayer.

* Factorio: Mirror chat sent from the FactorioClient UI to the Factorio server.

* Factorio: Remove local coordinates from outgoing chat.

* Factorio: Added setting to disable bridging chat out.
Added client command to toggle this setting at run-time.

* Factorio: Added in-game command to toggle chat bridging setting at run-time.

* .

* Factorio: Document toggle for chat bridging feature.

* (Removed superfluous type annotations.)

* (Removed hard to read regex.)

* Docs/Factorio: Fix display of multiline code snippets.
---
 FactorioClient.py                             | 53 ++++++++++++++++++-
 Utils.py                                      |  1 +
 host.yaml                                     |  2 +
 worlds/factorio/data/mod_template/control.lua |  6 +++
 worlds/factorio/docs/setup_en.md              | 27 ++++++----
 5 files changed, 76 insertions(+), 13 deletions(-)

diff --git a/FactorioClient.py b/FactorioClient.py
index 8ab9799b..12ec2291 100644
--- a/FactorioClient.py
+++ b/FactorioClient.py
@@ -8,6 +8,7 @@ import re
 import subprocess
 import time
 import random
+import typing
 
 import ModuleUpdate
 ModuleUpdate.update()
@@ -51,6 +52,9 @@ class FactorioCommandProcessor(ClientCommandProcessor):
         """Toggle filtering of item sends that get displayed in-game to only those that involve you."""
         self.ctx.toggle_filter_item_sends()
 
+    def _cmd_toggle_chat(self):
+        """Toggle sending of chat messages from players on the Factorio server to Archipelago."""
+        self.ctx.toggle_bridge_chat_out()
 
 class FactorioContext(CommonContext):
     command_processor = FactorioCommandProcessor
@@ -71,6 +75,8 @@ class FactorioContext(CommonContext):
         self.energy_link_increment = 0
         self.last_deplete = 0
         self.filter_item_sends: bool = False
+        self.multiplayer: bool = False  # whether multiple different players have connected
+        self.bridge_chat_out: bool = True
 
     async def server_auth(self, password_requested: bool = False):
         if password_requested and not self.password:
@@ -87,13 +93,15 @@ class FactorioContext(CommonContext):
     def on_print(self, args: dict):
         super(FactorioContext, self).on_print(args)
         if self.rcon_client:
-            self.print_to_game(args['text'])
+            if not args['text'].startswith(self.player_names[self.slot] + ":"):
+                self.print_to_game(args['text'])
 
     def on_print_json(self, args: dict):
         if self.rcon_client:
             if not self.filter_item_sends or not self.is_uninteresting_item_send(args):
                 text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
-                self.print_to_game(text)
+                if not text.startswith(self.player_names[self.slot] + ":"):
+                    self.print_to_game(text)
         super(FactorioContext, self).on_print_json(args)
 
     @property
@@ -130,6 +138,27 @@ class FactorioContext(CommonContext):
                                      f"{Utils.format_SI_prefix(args['value'])}J remaining.")
                         self.rcon_client.send_command(f"/ap-energylink {gained}")
 
+    def on_user_say(self, text: str) -> typing.Optional[str]:
+        # Mirror chat sent from the UI to the Factorio server.
+        self.print_to_game(f"{self.player_names[self.slot]}: {text}")
+        return text
+
+    async def chat_from_factorio(self, user: str, message: str) -> None:
+        if not self.bridge_chat_out:
+            return
+
+        # Pass through commands
+        if message.startswith("!"):
+            await self.send_msgs([{"cmd": "Say", "text": message}])
+            return
+
+        # Omit messages that contain local coordinates
+        if "[gps=" in message:
+            return
+
+        prefix = f"({user}) " if self.multiplayer else ""
+        await self.send_msgs([{"cmd": "Say", "text": f"{prefix}{message}"}])
+
     def toggle_filter_item_sends(self) -> None:
         self.filter_item_sends = not self.filter_item_sends
         if self.filter_item_sends:
@@ -139,6 +168,15 @@ class FactorioContext(CommonContext):
         logger.info(announcement)
         self.print_to_game(announcement)
 
+    def toggle_bridge_chat_out(self) -> None:
+        self.bridge_chat_out = not self.bridge_chat_out
+        if self.bridge_chat_out:
+            announcement = "Chat is now bridged to Archipelago."
+        else:
+            announcement = "Chat is no longer bridged to Archipelago."
+        logger.info(announcement)
+        self.print_to_game(announcement)
+
     def run_gui(self):
         from kvui import GameManager
 
@@ -178,6 +216,7 @@ async def game_watcher(ctx: FactorioContext):
                     research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
                     victory = data["victory"]
                     await ctx.update_death_link(data["death_link"])
+                    ctx.multiplayer = data.get("multiplayer", False)
 
                     if not ctx.finished_game and victory:
                         await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
@@ -281,8 +320,14 @@ async def factorio_server_watcher(ctx: FactorioContext):
                 elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-send-filter", msg):
                     factorio_server_logger.debug(msg)
                     ctx.toggle_filter_item_sends()
+                elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-chat$", msg):
+                    factorio_server_logger.debug(msg)
+                    ctx.toggle_bridge_chat_out()
                 else:
                     factorio_server_logger.info(msg)
+                    match = re.match(r"^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[CHAT\] ([^:]+): (.*)$", msg)
+                    if match:
+                        await ctx.chat_from_factorio(match.group(1), match.group(2))
             if ctx.rcon_client:
                 commands = {}
                 while ctx.send_index < len(ctx.items_received):
@@ -383,6 +428,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
 async def main(args):
     ctx = FactorioContext(args.connect, args.password)
     ctx.filter_item_sends = initial_filter_item_sends
+    ctx.bridge_chat_out = initial_bridge_chat_out
     ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
 
     if gui_enabled:
@@ -438,6 +484,9 @@ if __name__ == '__main__':
     if not isinstance(options["factorio_options"]["filter_item_sends"], bool):
         logging.warning(f"Warning: Option filter_item_sends should be a bool.")
     initial_filter_item_sends = bool(options["factorio_options"]["filter_item_sends"])
+    if not isinstance(options["factorio_options"]["bridge_chat_out"], bool):
+        logging.warning(f"Warning: Option bridge_chat_out should be a bool.")
+    initial_bridge_chat_out = bool(options["factorio_options"]["bridge_chat_out"])
 
     if not os.path.exists(os.path.dirname(executable)):
         raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
diff --git a/Utils.py b/Utils.py
index b2e98358..e0c86ddb 100644
--- a/Utils.py
+++ b/Utils.py
@@ -232,6 +232,7 @@ def get_default_options() -> OptionsType:
         "factorio_options": {
             "executable": os.path.join("factorio", "bin", "x64", "factorio"),
             "filter_item_sends": False,
+            "bridge_chat_out": True,
         },
         "sni_options": {
             "sni": "SNI",
diff --git a/host.yaml b/host.yaml
index 0bdd9535..4e94a9a3 100644
--- a/host.yaml
+++ b/host.yaml
@@ -101,6 +101,8 @@ factorio_options:
   # server_settings: "factorio\\data\\server-settings.json"
   # Whether to filter item send messages displayed in-game to only those that involve you.
   filter_item_sends: false
+  # Whether to send chat messages from players on the Factorio server to Archipelago.
+  bridge_chat_out: true
 minecraft_options: 
   forge_directory: "Minecraft Forge server"
   max_heap_size: "2G"
diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua
index 86e83b9f..98c9ca62 100644
--- a/worlds/factorio/data/mod_template/control.lua
+++ b/worlds/factorio/data/mod_template/control.lua
@@ -157,6 +157,7 @@ function on_player_created(event)
 {%- if silo == 2 %}
     check_spawn_silo(game.players[event.player_index].force)
 {%- endif %}
+    dumpInfo(player.force)
 end
 script.on_event(defines.events.on_player_created, on_player_created)
 
@@ -491,6 +492,7 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress
         ["death_link"] = DEATH_LINK,
         ["energy"] = chain_lookup(forcedata, "energy"),
         ["energy_bridges"] = chain_lookup(forcedata, "energy_bridges"),
+        ["multiplayer"] = #game.players > 1,
     }
 
     for tech_name, tech in pairs(force.technologies) do
@@ -600,5 +602,9 @@ commands.add_command("toggle-ap-send-filter", "Toggle filtering of item sends th
     log("Player command toggle-ap-send-filter") -- notifies client
 end)
 
+commands.add_command("toggle-ap-chat", "Toggle sending of chat messages from players on the Factorio server to Archipelago.", function(call)
+    log("Player command toggle-ap-chat") -- notifies client
+end)
+
 -- data
 progressive_technologies = {{ dict_to_lua(progressive_technology_table) }}
diff --git a/worlds/factorio/docs/setup_en.md b/worlds/factorio/docs/setup_en.md
index 8b24da13..73ff5c8c 100644
--- a/worlds/factorio/docs/setup_en.md
+++ b/worlds/factorio/docs/setup_en.md
@@ -132,6 +132,8 @@ This allows you to host your own Factorio game.
 
 For additional client features, issue the `/help` command in the Archipelago Client. Once connected to the AP server,
 you can also issue the `!help` command to learn about additional commands like `!hint`.
+For more information about the commands you can use, see the [Commands Guide](/tutorial/Archipelago/commands/en) and
+[Other Settings](#other-settings).
 
 ## Allowing Other People to Join Your Game
 
@@ -148,10 +150,20 @@ you can also issue the `!help` command to learn about additional commands like `
   - Type `/toggle-ap-send-filter` in-game
   - Type `/toggle_send_filter` in the Archipelago Client
   - In your `host.yaml` set
-    ```
-    factorio_options:
-      filter_item_sends: true
-    ```
+```
+factorio_options:
+  filter_item_sends: true
+```
+- By default, in-game chat is bridged to Archipelago. If you prefer to be able to speak privately, you can disable this
+  feature by doing one of the following:
+  - Type `/toggle-ap-chat` in-game
+  - Type `/toggle_chat` in the Archipelago Client
+  - In your `host.yaml` set
+```
+factorio_options:
+  bridge_chat_out: false
+```
+  Note that this will also disable `!` commands from within the game, and that it will not affect incoming chat.
 
 ## Troubleshooting
 
@@ -159,13 +171,6 @@ In case any problems should occur, the Archipelago Client will create a file `Fa
 contents of this file may help you troubleshoot an issue on your own and is vital for requesting help from other people
 in Archipelago.
 
-## Commands in game
-
-Once you have connected to the server successfully using the Archipelago Factorio Client you should see a message
-stating you can get help using Archipelago commands by typing `!help`. Commands cannot currently be sent from within
-the Factorio session, but you can send them from the Archipelago Factorio Client. For more information about the commands
-you can use see the [commands guide](/tutorial/Archipelago/commands/en).
-
 ## Additional Resources
 
 - Alternate Tutorial by