This currently only affects system colonisation ships, which have '$EXT_PANEL_ColonisationShip;' in their StationName
entries in the journal. Other strings may need to be added in future.
Fixes#2404
diff --git a/L10n/en.template b/L10n/en.template
index faac3259..f670e39c 100644
--- a/L10n/en.template
+++ b/L10n/en.template
@@ -839,3 +839,9 @@
/* edastro_core.py: Settings>EDAstro - Label on checkbox for 'send data'; */
"Send data to EDAstro" = "Send data to EDAstro";
+
+/* common_coreutils.py: Colonisation ship name, with title; In files: common_coreutils.py:81:81 */
+"System Colonisation Ship {NAME}" = "System Colonisation Ship {NAME}";
+
+/* common_coreutils.py: Unidentified colonisation ship; In files: common_coreutils.py:84:84 */
+"System Colonisation Ship" = "System Colonisation Ship";
diff --git a/plugins/common_coreutils.py b/plugins/common_coreutils.py
index 6b90d4dc..a488de46 100644
--- a/plugins/common_coreutils.py
+++ b/plugins/common_coreutils.py
@@ -29,6 +29,7 @@ import gzip
import io
import json
import os
+import re
import myNotebook as nb # noqa: N813
from EDMCLogging import get_main_logger
from companion import CAPIData
@@ -47,6 +48,49 @@ SEPY = 10 # seperator line spacing
STATION_UNDOCKED = '×' # "Station" name to display when not docked = U+00D7
+# Known colonisation ship names match one of the following:
+# "$EXT_PANEL_ColonisationShip;"
+# "$EXT_PANEL_ColonisationShip; <ship name>" with "<ship name>" being eg; "Okawara Horizons"
+# "$EXT_PANEL_ColonisationShip:#index=<number>;"
+# "$EXT_PANEL_ColonisationShip:#index=<number>; <ship name>"
+_RE_COLONISATION_SHIP = re.compile(r'\$EXT_PANEL_ColonisationShip(?::#index=(\d+))?;(?: (.+))?')
+
+
+def localised_name(value: str | None) -> str | None:
+ """
+ Handle localisation of strings in places like station names.
+
+ This is currently an issue for system colonisation ships that have a station name along the
+ lines of "$EXT_PANEL_ColonisationShip; Okawara Horizons" when written to the journal, but would
+ be better displayed as "System Colonisation Ship Okawara Horizona" (or other appropriately
+ localised equivalent).
+
+ If no (known) localisable strings are present, the input will be returned unmodified.
+
+ :param value: The non-localised string.
+ :return: The localised string, or None if the input was None.
+ """
+ if value is None:
+ return None
+
+ colonisation_ship_match = _RE_COLONISATION_SHIP.match(value)
+ if colonisation_ship_match:
+ ship_index = colonisation_ship_match.group(1)
+ ship_name = colonisation_ship_match.group(2)
+ # Use the ship's index as its name if no name was provided.
+ ship_id = ship_name or ship_index
+
+ if ship_id:
+ # LANG: Colonisation ship name, with title
+ value = tr.tl("System Colonisation Ship {NAME}").format(NAME=ship_id)
+
+ else:
+ # LANG: Unidentified colonisation ship
+ value = tr.tl("System Colonisation Ship")
+
+ return value
+
+
def plugin_start3(plugin_dir: str) -> str:
"""
Start the plugin.
@@ -121,7 +165,7 @@ def station_link_common(data: CAPIData, this):
:param this: The module global from the calling module.
"""
if data['commander']['docked'] or this.on_foot and this.station_name:
- this.station_link['text'] = this.station_name
+ this.station_link['text'] = localised_name(this.station_name)
elif data['lastStarport']['name'] and data['lastStarport']['name'] != "":
this.station_link['text'] = STATION_UNDOCKED
else:
@@ -163,12 +207,14 @@ def station_name_setter_common(this):
:param this: The module global from the calling module.
"""
to_set: str = cast(str, this.station_name)
+
if not to_set:
if this.system_population is not None and this.system_population > 0:
to_set = STATION_UNDOCKED
else:
to_set = ''
+ to_set = str(localised_name(to_set))
this.station_link['text'] = to_set
diff --git a/plugins/edsm.py b/plugins/edsm.py
index e1e6b168..902e248b 100644
--- a/plugins/edsm.py
+++ b/plugins/edsm.py
@@ -43,7 +43,7 @@ from ttkHyperlinkLabel import HyperlinkLabel
from l10n import translations as tr
from plugins.common_coreutils import (api_keys_label_common, PADX, PADY, BUTTONX, SEPY, BOXY, STATION_UNDOCKED,
show_pwd_var_common, station_link_common, this_format_common,
- cmdr_data_initial_common)
+ cmdr_data_initial_common, localised_name)
# TODO:
@@ -543,7 +543,7 @@ entry: {entry!r}'''
to_set = ''
if this.station_link:
- this.station_link['text'] = to_set
+ this.station_link['text'] = localised_name(to_set)
this.station_link['url'] = station_url(str(this.system_name), str(this.station_name))
this.station_link.update_idletasks()
Changing game mode would otherwise re-use the previously used game mode for events before the new LoadGame event. This
resets the mode on the first available event so they are buffered again until the new game mode is determined.
Mostly related to adjusting `if sys.platform == 'win32'` logic so things that are only defined on win32 platforms don't
cause issues on other platforms.
Done by temporarily buffering events before monitor.mode is set, then notifying plugins about them before sending the
first event where monitor.mode has a value. LoadGame is currently the only event that sets monitor.mode.
Fixes#2286
diff --git a/EDMarketConnector.py b/EDMarketConnector.py
index e7bdf490..d53b625d 100644
--- a/EDMarketConnector.py
+++ b/EDMarketConnector.py
@@ -22,7 +22,7 @@ import threading
import webbrowser
from os import chdir, environ
from time import localtime, strftime, time
-from typing import TYPE_CHECKING, Any, Literal
+from typing import TYPE_CHECKING, Any, Literal, MutableMapping
from constants import applongname, appname, protocolhandler_redirect
# Have this as early as possible for people running EDMarketConnector.exe
@@ -458,6 +458,8 @@ class AppWindow:
def __init__(self, master: tk.Tk): # noqa: C901, CCR001 # TODO - can possibly factor something out
+ self.early_journal_events: list[MutableMapping[str, Any]] = []
+
self.capi_query_holdoff_time = config.get_int('querytime', default=0) + companion.capi_query_cooldown
self.capi_fleetcarrier_query_holdoff_time = config.get_int('fleetcarrierquerytime', default=0) \
+ companion.capi_fleetcarrier_query_cooldown
@@ -1400,6 +1402,19 @@ class AppWindow:
self.cooldown()
logger.trace_if('capi.worker', '...done')
+ def handle_plugin_event_error(self, err: str | None) -> None:
+ """
+ Display a plugin's error message at the bottom of the main window.
+
+ If there was no error message passed in, nothing will be done.
+
+ :param err: Message to display, or None for no error.
+ """
+ if err:
+ self.status['text'] = err
+ if not config.get_int('hotkey_mute'):
+ hotkeymgr.play_bad()
+
def journal_event(self, event: str): # noqa: C901, CCR001 # Currently not easily broken up.
"""
Handle a Journal event passed through event queue from monitor.py.
@@ -1513,15 +1528,21 @@ class AppWindow:
if monitor.cmdr and monitor.mode == 'CQC' and entry['event']:
err = plug.notify_journal_entry_cqc(monitor.cmdr, monitor.is_beta, entry, monitor.state)
- if err:
- self.status['text'] = err
- if not config.get_int('hotkey_mute'):
- hotkeymgr.play_bad()
+ self.handle_plugin_event_error(err)
return # in CQC
if not entry['event'] or not monitor.mode:
+ # Game session has not fully loaded yet. We may be receiving events from the
+ # journal, but don't know which game mode (Open/CQC/solo etc) we're running in.
logger.trace_if('journal.queue', 'Startup, returning')
+
+ if entry['event']:
+ # Since we don't know which mode the game is in (and therefore whether to send
+ # the event to a plugin's `journal_event` or `journal_event_cqc` function),
+ # temporarily queue it so we can pass it on once the mode is known.
+ self.early_journal_events.append(entry)
+
return # Startup
if entry['event'] in ('StartUp', 'LoadGame') and monitor.started:
@@ -1538,6 +1559,25 @@ class AppWindow:
if not dashboard.start(self.w, monitor.started):
logger.info("Can't start Status monitoring")
+ # monitor.cmdr should always be set if monitor.mode is set (and monitor.mode must be set
+ # if we reach this point in the function), but we check monitor.cmdr here so the mypy
+ # doesn't complain about passing str|None where str is required.
+ if self.early_journal_events and monitor.cmdr:
+ for early_entry in self.early_journal_events:
+ if monitor.mode == 'CQC':
+ err = plug.notify_journal_entry_cqc(monitor.cmdr, monitor.is_beta, early_entry, monitor.state)
+ else:
+ err = plug.notify_journal_entry(
+ monitor.cmdr,
+ monitor.is_beta,
+ monitor.state['SystemName'],
+ monitor.state['StationName'],
+ early_entry,
+ monitor.state
+ )
+ self.handle_plugin_event_error(err)
+ self.early_journal_events.clear()
+
# Export loadout
if entry['event'] == 'Loadout' and not monitor.state['Captain'] \
and config.get_int('output') & config.OUT_SHIP:
@@ -1553,10 +1593,7 @@ class AppWindow:
monitor.state
)
- if err:
- self.status['text'] = err
- if not config.get_int('hotkey_mute'):
- hotkeymgr.play_bad()
+ self.handle_plugin_event_error(err)
auto_update = False
# Only if auth callback is not pending
@@ -1638,11 +1675,7 @@ class AppWindow:
# Currently we don't do anything with these events
if monitor.cmdr:
err = plug.notify_dashboard_entry(monitor.cmdr, monitor.is_beta, entry)
-
- if err:
- self.status['text'] = err
- if not config.get_int('hotkey_mute'):
- hotkeymgr.play_bad()
+ self.handle_plugin_event_error(err)
def plugin_error(self, event=None) -> None:
"""Display asynchronous error from plugin."""
The `get_list` does a type check if the value returned is a list, and returns default otherwise. Since we were storing everything as a string, if something set a list after the initial migration to toml, that key would then always be returned as the default value
diff --git a/PLUGINS.md b/PLUGINS.md
index 6fd20ef9..71aaec7a 100644
--- a/PLUGINS.md
+++ b/PLUGINS.md
@@ -163,6 +163,19 @@ circular imports.
For an example of how this is done, look at the code in `plugins/common_coreutils.py`
and the usage of these functions in other core plugins.
+Plugins may import additional code modules from `load.py` in the usual way. Note
+that using an import statement like `import my_module` or `from my_module import
+foo` will only import one module with that name **across all plugins**. This can
+be used to create a shared library used by several plugins if used carefully,
+but may create a conflict if another plugin also has a module of the same name
+containing different code. For example if two plugins both contain a
+`widgets.py` file and both do `import widgets`, only one of the two modules will
+actually be imported.
+
+To avoid this issue, use a relative import statement such as `import .widgets`
+or `from .widgets import foo` to ensure that the module is loaded from the
+current plugin, even if the name conflicts with another plugin's code.
+
---
## Logging
Fixes a race identified where long plugin init times can result in
the following error being thrown in the journal monitor thread,
requiring a preferences reload to have journals continuing to be
processed:
```
Exception in thread Journal worker:
Traceback (most recent call last):
File "/home/testing/.pyenv/versions/3.11.9/lib/python3.11/threading.py", line 1045, in _bootstrap_inner
self.run()
File "/home/testing/.pyenv/versions/3.11.9/lib/python3.11/threading.py", line 982, in run
self._target(*self._args, **self._kwargs)
File "/home/testing/gitstuff/EDMarketConnector/monitor.py", line 461, in worker
self.root.event_generate('<<JournalEvent>>', when="tail")
File "/home/testing/.pyenv/versions/3.11.9/lib/python3.11/tkinter/__init__.py", line 1933, in event_generate
self.tk.call(args)
RuntimeError: main thread is not in main loop
```