1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-24 12:40:52 +03:00

Merge branch 'stable' into releases

This commit is contained in:
Athanasius 2021-05-21 15:30:13 +01:00
commit 1e79e59ba9
26 changed files with 606 additions and 131 deletions

View File

@ -1,6 +1,76 @@
This is the master changelog for Elite Dangerous Market Connector. Entries are in reverse chronological order (latest first).
---
* We now test against, and package with, Python 3.9.5.
**As a consequence of this we no longer support Windows 7.
This is due to
[Python 3.9.x itself not supporting Windows 7](https://www.python.org/downloads/windows/).
The application (both EDMarketConnector.exe and EDMC.exe) will crash on
startup due to a missing DLL.**
This should have no other impact on users or plugin developers, other
than the latter now being free to use features that were introduced since the
Python 3.7 series.
Developers can check the contents of the `.python-version` file
in the source (it's not distributed with the Windows installer) for the
currently used version in a given branch.
Release 5.0.2
===
This release is primarily aimed at getting the UI "`Suit: ...`" line working
properly.
* The "`Suit: ...`" UI line should now function as best it can given the
available data from the game. It should not appear if you have launched
the Horizons version of the game, even if your account has Odyssey
enabled. You might see "`<Unknown>`" as the text when this application
does not yet have the required data.
* Changed the less than obvious "`unable to get endpoint: /profile`" error
message to "`Frontier CAPI query failure: /profile`", and similarly for the
other CAPI endpoints we attempt to access. This new form is potentially
translated, but translators need time to do that.
In addition the old message "`Received error {r.status_code} from server`"
has been changed to "`Frontier CAPI server error: {r.status_code}`" and is
potentially translated.
* The filenames used for 'Market data in CSV format file' will now be sane,
and as they were before 5.0.0.
* Linux: 'Shipyard provider' will no longer default to showing 'False' if
no specific provider has been selected.
Plugin Developers
---
* Extra `Flagse` values added in the live release of Odyssey have been added to
`edmc_data.py`.
* Odyssey 'BackPack' values should now track better, but might still not be
perfect due to Journal bugs/shortcomings.
* `state` passed to `journal_entry()` now has a `BackpackJSON` (note the case)
member which is a copy of the data from the `Backpack.json` (yes, that's
currently the correct case) file that is written when there's a `BackPack`
(guess what, yes, that is currently the correct case) event written to
the Journal.
* `state['Credits']` tracking is almost certainly not perfect. We're
accounting for the credits component of `SuitUpgrade` now, but there
might be other such we've yet accounted for.
* `state['Suits']` and associated other keys should now be tracking from
Journal events, where possible, as well as CAPI data.
* There is a section in PLUGINS.md about how to package an extra Python
module with your plugin. Note the new caveat in
[PLUGINS.md:Avoiding-pitfalls](./PLUGINS.md#avoiding-potential-pitfalls)
about the name of your plugin's directory.
Release 5.0.1
===

View File

@ -620,6 +620,11 @@ class AppWindow(object):
def update_suit_text(self) -> None:
"""Update the suit text for current type and loadout."""
if not monitor.state['Odyssey']:
# Odyssey not detected, no text should be set so it will hide
self.suit['text'] = ''
return
if (suit := monitor.state.get('SuitCurrent')) is None:
self.suit['text'] = f'<{_("Unknown")}>'
return
@ -633,6 +638,14 @@ class AppWindow(object):
loadout_name = suitloadout['name']
self.suit['text'] = f'{suitname} ({loadout_name})'
def suit_show_if_set(self) -> None:
"""Show UI Suit row if we have data, else hide."""
if self.suit['text'] != '':
self.toggle_suit_row(visible=True)
else:
self.toggle_suit_row(visible=False)
def toggle_suit_row(self, visible: Optional[bool] = None) -> None:
"""
Toggle the visibility of the 'Suit' row.
@ -922,10 +935,7 @@ class AppWindow(object):
self.suit['text'] = f'{suitname} ({loadout_name})'
self.toggle_suit_row(visible=True)
else:
self.toggle_suit_row(visible=False)
self.suit_show_if_set()
if data['commander'].get('credits') is not None:
monitor.state['Credits'] = data['commander']['credits']
@ -1052,6 +1062,7 @@ class AppWindow(object):
self.cmdr['text'] += ' (beta)'
self.update_suit_text()
self.suit_show_if_set()
self.edit_menu.entryconfigure(0, state=monitor.system and tk.NORMAL or tk.DISABLED) # Copy

View File

@ -1,6 +1,3 @@
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Chyba: Frontier CAPI neodpovídá";
/* Language name */
"!Language" = "Čeština";
@ -185,6 +182,9 @@
"Error: EDSM {MSG}" = "Chyba: EDSM {MSG}";
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Chyba: Frontier CAPI neodpovídá";
/* OLD: Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier server is down" = "Chyba: Server Frontieru nejede";

View File

@ -1,6 +1,3 @@
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Fehler: Frontier CAPI antwortet nicht";
/* Language name */
"!Language" = "Deutsch";
@ -185,6 +182,9 @@
"Error: EDSM {MSG}" = "Fehler: EDSM {MSG}";
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Fehler: Frontier CAPI antwortet nicht";
/* OLD: Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier server is down" = "Fehler: Frontier-Server ist offline";

View File

@ -230,6 +230,12 @@
/* Section heading in settings. [prefs.py] */
"File location" = "File location";
/* Failures to access Frontier CAPI endpoints [companion.py] */
"Frontier CAPI query failure" = "Frontier CAPI query failure";
/* Server errors from Frontier CAPI server [companion.py] */
"Frontier CAPI server error" = "Frontier CAPI server error";
/* CQC rank. [stats.py] */
"Gladiator" = "Gladiator";

View File

@ -124,6 +124,15 @@
/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */
"E:D journal file location" = "Localización del archivo de Journal de E:D";
/* When EDDB functionality has been killswitched. [plugins/eddb.py] */
"EDDB Journal processing disabled. See Log." = "Procesamiento de Journal para EDDB desactivado. Ver Log.";
/* When EDDN journal handling has been killswitched [plugins/eddn.py] */
"EDDN journal handler disabled. See Log." = "Manejo de Journal para EDDN desactivado. Ver Log.";
/* When EDSM functionality has been killswitched [plugins/edsm.py] */
"EDSM Handler disabled. See Log." = "Manejo de Journal para EDSM desactivado. Ver Log.";
/* Empire rank. [stats.py] */
"Earl" = "Conde Palatino";
@ -173,6 +182,9 @@
"Error: EDSM {MSG}" = "Error: EDSM {MSG}";
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Error: El CAPI de Frontier no respondió";
/* OLD: Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier server is down" = "Error: El servidor de Frontier está caído";
@ -251,6 +263,9 @@
/* Section heading in settings. [inara.py] */
"Inara credentials" = "Credenciales de Inara";
/* When Inara support has been killswitched [plugins/inara.py] */
"Inara disabled. See Log." = "Inara desactivado. Ver Log.";
/* Settings>Plugins>Information on migrating plugins [prefs.py] */
"Information on migrating plugins" = "Información sobre migración de plugins";
@ -506,6 +521,9 @@
/* Menu item. [EDMarketConnector.py] */
"Status" = "Estado";
/* 'Suit' label in main UI' [EDMarketConnector.py] */
"Suit" = "Traje";
/* Explorer rank. [stats.py] */
"Surveyor" = "Topógrafo";

View File

@ -124,6 +124,15 @@
/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */
"E:D journal file location" = "E:Dゲームクライアントのジャーナルファイル出力先";
/* When EDDB functionality has been killswitched. [plugins/eddb.py] */
"EDDB Journal processing disabled. See Log." = "EDDBジャーナル処理が無効です。ログを確認してください。";
/* When EDDN journal handling has been killswitched [plugins/eddn.py] */
"EDDN journal handler disabled. See Log." = "EDDNジャーナルハンドラーが無効です。ログを確認してください。";
/* When EDSM functionality has been killswitched [plugins/edsm.py] */
"EDSM Handler disabled. See Log." = "EDSMハンドラーが無効です。ログを確認してください。";
/* Empire rank. [stats.py] */
"Earl" = "Earl";
@ -173,6 +182,9 @@
"Error: EDSM {MSG}" = "エラー: EDSM {MSG}";
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "エラー: Frontier CAPIからの応答がありません";
/* OLD: Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier server is down" = "エラー: Frontier社のサーバがダウンしています";
@ -218,6 +230,12 @@
/* Section heading in settings. [prefs.py] */
"File location" = "ファイルの出力先";
/* Failures to access Frontier CAPI endpoints [companion.py] */
"Frontier CAPI query failure" = "Frontier CAPIクエリ失敗";
/* Server errors from Frontier CAPI server [companion.py] */
"Frontier CAPI server error" = "Frontier CAPIサーバエラー";
/* CQC rank. [stats.py] */
"Gladiator" = "Gladiator";
@ -251,6 +269,9 @@
/* Section heading in settings. [inara.py] */
"Inara credentials" = "Inara認証情報";
/* When Inara support has been killswitched [plugins/inara.py] */
"Inara disabled. See Log." = "Inaraが無効です。ログを確認してください。";
/* Settings>Plugins>Information on migrating plugins [prefs.py] */
"Information on migrating plugins" = "プラグインの移行に関する情報";

View File

@ -124,6 +124,15 @@
/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */
"E:D journal file location" = "E:D 저널(journal) 파일 위치";
/* When EDDB functionality has been killswitched. [plugins/eddb.py] */
"EDDB Journal processing disabled. See Log." = "EDDB 저널 처리 비활성화됨. 로그 참고 바람.";
/* When EDDN journal handling has been killswitched [plugins/eddn.py] */
"EDDN journal handler disabled. See Log." = "EDDN 저널 처리 비활성화됨. 로그 참고 바람.";
/* When EDSM functionality has been killswitched [plugins/edsm.py] */
"EDSM Handler disabled. See Log." = "EDSM Handler 비활성화됨. 로그 참고 바람.";
/* Empire rank. [stats.py] */
"Earl" = "Earl";
@ -173,6 +182,9 @@
"Error: EDSM {MSG}" = "오류: EDSM {MSG}";
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "오류: Frontier CAPI가 응답이 없음";
/* OLD: Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier server is down" = "오류: Frontier 서버 CAPI 연결 불가";
@ -251,6 +263,9 @@
/* Section heading in settings. [inara.py] */
"Inara credentials" = "Inara 계정 정보";
/* When Inara support has been killswitched [plugins/inara.py] */
"Inara disabled. See Log." = "Inara 비활성화됨. 로그 참고 바람.";
/* Settings>Plugins>Information on migrating plugins [prefs.py] */
"Information on migrating plugins" = "플러그인 위치 변경에 대한 정보";

View File

@ -124,6 +124,15 @@
/* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */
"E:D journal file location" = "E:D Lokacja pliku dziennika";
/* When EDDB functionality has been killswitched. [plugins/eddb.py] */
"EDDB Journal processing disabled. See Log." = "Wyłączone przetwarzanie dziennika EDDB. Sprawdź logi.";
/* When EDDN journal handling has been killswitched [plugins/eddn.py] */
"EDDN journal handler disabled. See Log." = "Wyłączona obsługa dziennika EDDB. Sprawdź logi.";
/* When EDSM functionality has been killswitched [plugins/edsm.py] */
"EDSM Handler disabled. See Log." = "Wyłączona obsługa EDDB. Sprawdź logi.";
/* Empire rank. [stats.py] */
"Earl" = "Earl";
@ -173,6 +182,9 @@
"Error: EDSM {MSG}" = "Błąd: EDSM {MSG}";
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Błąd: Brak odpowiedzi od CAPI Frontier";
/* OLD: Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier server is down" = "Błąd: Serwer Frontiera nie odpowiada";
@ -251,6 +263,9 @@
/* Section heading in settings. [inara.py] */
"Inara credentials" = "Inara - uprawnienia";
/* When Inara support has been killswitched [plugins/inara.py] */
"Inara disabled. See Log." = "Inara jest wyłączona. Sprawdź logi.";
/* Settings>Plugins>Information on migrating plugins [prefs.py] */
"Information on migrating plugins" = "Informacja o migracji pluginów";

View File

@ -1,6 +1,3 @@
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Erro: o CAPI da Frontier não respondeu";
/* Language name */
"!Language" = "Português (Brasil)";
@ -185,6 +182,9 @@
"Error: EDSM {MSG}" = "Erro: EDSM {MSG}";
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Erro: o CAPI da Frontier não respondeu";
/* OLD: Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier server is down" = "Erro: O servidor da Frontier está offline";
@ -230,6 +230,12 @@
/* Section heading in settings. [prefs.py] */
"File location" = "Localização do Arquivo";
/* Failures to access Frontier CAPI endpoints [companion.py] */
"Frontier CAPI query failure" = "Falha na requisição ao CAPI da Frontier";
/* Server errors from Frontier CAPI server [companion.py] */
"Frontier CAPI server error" = "Erro de servidor do CAPI da Frontier";
/* CQC rank. [stats.py] */
"Gladiator" = "Gladiador";

View File

@ -1,6 +1,3 @@
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Erro: CAPI da Frontier não respondeu.";
/* Language name */
"!Language" = "Português (Portugal)";
@ -185,6 +182,9 @@
"Error: EDSM {MSG}" = "Erro: EDSM {MSG}";
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Erro: CAPI da Frontier não respondeu.";
/* OLD: Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier server is down" = "Erro: O servidor da Frontier está offline";
@ -230,6 +230,12 @@
/* Section heading in settings. [prefs.py] */
"File location" = "Localização do Ficheiro";
/* Failures to access Frontier CAPI endpoints [companion.py] */
"Frontier CAPI query failure" = "Falha ao consultar a CAPI da Frontier.";
/* Server errors from Frontier CAPI server [companion.py] */
"Frontier CAPI server error" = "Erro do servidor CAPI da Frontier.";
/* CQC rank. [stats.py] */
"Gladiator" = "Gladiador";

View File

@ -1,6 +1,3 @@
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Ошибка: CAPI Frontier не отвечает";
/* Language name */
"!Language" = "Русский";
@ -185,6 +182,9 @@
"Error: EDSM {MSG}" = "Ошибка: EDSM {MSG}";
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Ошибка: CAPI Frontier не отвечает";
/* OLD: Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier server is down" = "Ошибка: сервера Frontier недоступны";

View File

@ -1,6 +1,3 @@
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Greška: Frontier CAPI nije odgovorio";
/* Language name */
"!Language" = "Srpski (Latinica, Bosna i Hercegovina)";
@ -185,6 +182,9 @@
"Error: EDSM {MSG}" = "Greška: EDSM {MSG}";
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Greška: Frontier CAPI nije odgovorio";
/* OLD: Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier server is down" = "Greška: Frontier server je nedostupan";

View File

@ -1,6 +1,3 @@
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Greška: Frontier CAPI nije odgovorio";
/* Language name */
"!Language" = "Srpski (Latinica)";
@ -185,6 +182,9 @@
"Error: EDSM {MSG}" = "Greška: EDSM {MSG}";
/* Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier CAPI didn't respond" = "Greška: Frontier CAPI nije odgovorio";
/* OLD: Raised when cannot contact the Companion API server. [companion.py] */
"Error: Frontier server is down" = "Greška: Frontier server je nedostupan";

View File

@ -270,6 +270,11 @@ There are a number of things that your code should either do or avoiding
doing so as to play nicely with the core EDMC code and not risk causing
application crashes or hangs.
### Be careful about the name of your plugin directory
You might want your plugin directory name to be usable in import statements.
See the section on [packaging extra modules](#your-plugin-directory-name-must-be-importable).
### Use a thread for long-running code
By default, your plugin code will be running in the main thread. So, if you
@ -594,6 +599,7 @@ Content of `state` (updated to the current journal entry):
| `Consumable` | `dict` | 'Consumable' MicroResources in Odyssey, `int` count each. |
| `Data` | `dict` | 'Data' MicroResources in Odyssey, `int` count each. |
| `BackPack` | `dict` | `dict` of Odyssey MicroResources in backpack. |
| `BackpackJSON` | `dict` | Content of Backpack.json as of last read. |
| `SuitCurrent` | `dict` | CAPI-returned data of currently worn suit. NB: May be `None` if no data. |
| `Suits` | `dict`[1] | CAPI-returned data of owned suits. NB: May be `None` if no data. |
| `SuitLoadoutCurrent` | `dict` | CAPI-returned data of current Suit Loadout. NB: May be `None` if no data. |
@ -924,12 +930,91 @@ plugin's folder:
&rarr; Compressed (zipped) folder.
- Mac: In Finder right click on your plugin's folder and choose Compress.
If there are any external dependencies then include them in the plugin's
folder.
If there are any external dependencies then
[include them](#packaging-extra-modules) in the plugin's folder.
Optionally, for tidiness delete any `.pyc` and `.pyo` files in the archive, as
well as the `__pycache__` directory.
---
## Packaging extra modules
EDMarketConnector's Windows installs only package a minimal set of modules.
All of the 'stdlib' of Python is provided, plus any modules the core
application code uses and a small number of additional modules for the
use of plugins. See
[Plugins:Available imports](https://github.com/EDCD/EDMarketConnector/blob/main/PLUGINS.md#available-imports)
for a list.
As such if your plugin requires additional modules you will need to package
them with your plugin. There is no general Python interpreter in which to
rely on [pip](https://pypi.org/project/pip/) to install them.
### Environment
It will be easier of you are using a Python
[virtual environment](https://docs.python.org/3/library/venv.html) for
actually testing the plugin. This is so that you can be sure it is working
*because* you have copied all the correct Python modules inside your
plugin, and not because they are installed within the Python site-packages
in some applicable location (system level or user level).
So, setup a virtual environment to use when running EDMarketConnector
code to test your plugin, and use the 'system' non-virtual Python to
install modules in order to have somewhere to copy them from.
NB: If you use PyCharm it's possible to have it do the work of
[creating a virtual environment](https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html)
for your project.
### Install the modules for the system Python
Technically you could also do this within an additional virtual environment.
If they were in your plugin testing virtual environment then you can't be
sure you have all the necessary files copied into your plugin so it will
work within a vanilla Windows EDMarketConnector install.
We'll use `xml_dataclasses` for this example.
pip install xml_dataclasses
### Copy the module files into your plugin directory
1. Assuming it's a 'simple' module with no caveats, now we copy:
1. `pip show xml_dataclasses` - `Location` is where it was installed.
1. If you have a POSIX-compliant command-line environment:
cp -pr <Location> <plugin_dir>
or just use Windows File Explorer, or other GUI means, to copy.
### Your plugin directory name **must** be importable
You're going to have to refer to your plugin directory in order to import
anything within it. This means it should be compatible with such.
1. Do **not** use hyphens (`-`) as word separators, or full-stops (`.`).
1. You can use underscore (`_`) as a word separator.
So:
- `EDMC-My-Plugin` **BAD**.
- `EDMC.My.Plugin` **BAD**.
- `EDMC_My_Plugin` **GOOD**.
NB: No, you can't use `from . import xml_dataclasses` because the way
EDMarketConnector:plug.py loads 'found' plugins prevents this from working.
### Test the module import
Add an import of this module to your plugin code:
from EDMC_My_Plugin import xml_dataclasses
If you're lucky you won't have the "surprise!" of learning your chosen
extra module itself requires other modules. If you are gifted such a surprise
then you will need to repeat the [Copy](#copy-the-module-files-into-your-plugin-directory)
step for the extra module(s) until it works.
---
## Disable a plugin

View File

@ -24,9 +24,9 @@ def export(data, kind=COMMODITY_DEFAULT, filename=None) -> None:
querytime = config.get_int('querytime', default=int(time.time()))
if not filename:
filename_system = data['lastSystem']['name'].strip(),
filename_starport = data['lastStarport']['name'].strip(),
filename_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime)),
filename_system = data['lastSystem']['name'].strip()
filename_starport = data['lastStarport']['name'].strip()
filename_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime))
filename_kind = 'csv'
filename = f'{filename_system}.{filename_starport}.{filename_time}.{filename_kind}'
filename = join(config.get_str('outdir'), filename)

View File

@ -541,7 +541,7 @@ class Session(object):
except Exception as e:
logger.debug('Attempting GET', exc_info=e)
raise ServerError(f'unable to get endpoint {endpoint}') from e
raise ServerError(f'{_("Frontier CAPI query failure")}: {endpoint}') from e
if r.url.startswith(SERVER_AUTH):
logger.info('Redirected back to Auth Server')
@ -556,7 +556,7 @@ class Session(object):
# Server error. Typically 500 "Internal Server Error" if server is down
logger.debug('500 status back from CAPI')
self.dump(r)
raise ServerError(f'Received error {r.status_code} from server')
raise ServerError(f'{_("Frontier CAPI server error")}: {r.status_code}')
try:
r.raise_for_status() # Typically 403 "Forbidden" on token expiry

View File

@ -33,7 +33,7 @@ appcmdname = 'EDMC'
# <https://semver.org/#semantic-versioning-specification-semver>
# Major.Minor.Patch(-prerelease)(+buildmetadata)
# NB: Do *not* import this, use the functions appversion() and appversion_nobuild()
_static_appversion = '5.0.1'
_static_appversion = '5.0.2'
copyright = '© 2015-2019 Jonathan Harris, 2020-2021 EDCD'
update_feed = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml'
@ -96,9 +96,21 @@ def git_shorthash_from_head() -> str:
"""
Determine short hash for current git HEAD.
Includes -DIRTY if any changes have been made from HEAD
:return: str - None if we couldn't determine the short hash.
"""
shorthash: str = None # type: ignore
dirty = False
with contextlib.suppress(Exception):
result = subprocess.run('git diff --stat HEAD'.split(), capture_output=True)
if len(result.stdout) > 0:
dirty = True
if len(result.stderr) > 0:
logger.warning(f'Data from git on stderr:\n{str(result.stderr)}')
try:
git_cmd = subprocess.Popen('git rev-parse --short HEAD'.split(),
stdout=subprocess.PIPE,
@ -115,7 +127,7 @@ def git_shorthash_from_head() -> str:
logger.error(f"'{shorthash}' doesn't look like a valid git short hash, forcing to None")
shorthash = None # type: ignore
return shorthash
return shorthash + ('-WORKING-DIR-IS-DIRTY' if dirty else '')
def appversion() -> semantic_version.Version:

View File

@ -429,6 +429,11 @@ Flags2Cold = 1 << 8
Flags2Hot = 1 << 9
Flags2VeryCold = 1 << 10
Flags2VeryHot = 1 << 11
Flags2GlideMode = 1 << 12
Flags2OnFootInHangar = 1 << 13
Flags2OnFootSocialSpace = 1 << 14
Flags2OnFootExterior = 1 << 15
Flags2BreathableAtmosphere = 1 << 16
# Dashboard GuiFocus constants
GuiFocusNoFocus = 0

View File

@ -32,6 +32,14 @@ def export(data, filename=None):
querytime = config.get_int('querytime', default=int(time.time()))
# Write
#
# When this is refactored into multi-line CHECK IT WORKS, avoiding the
# brainfart we had with dangling commas in commodity.py:export() !!!
#
filename = join(config.get_str('outdir'), '%s.%s.txt' % (ship, time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime))))
#
# When this is refactored into multi-line CHECK IT WORKS, avoiding the
# brainfart we had with dangling commas in commodity.py:export() !!!
#
with open(filename, 'wt') as h:
h.write(string)

View File

@ -151,6 +151,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
'Item': defaultdict(int), # BackPack Items
'Data': defaultdict(int), # Backpack Data
},
'BackpackJSON': None, # Raw JSON from `Backpack.json` file, if available
'SuitCurrent': None,
'Suits': {},
'SuitLoadoutCurrent': None,
@ -762,20 +763,24 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
self.state[event_type] = payload
elif event_type == 'EngineerProgress':
engineers = self.state['Engineers']
if 'Engineers' in entry: # Startup summary
self.state['Engineers'] = {
e['Engineer']: ((e['Rank'], e.get('RankProgress', 0)) if 'Rank' in e else e['Progress'])
for e in entry['Engineers']
}
# Sanity check - at least once the 'Engineer' (name) was missing from this in early
# Odyssey 4.0.0.100. Might only have been a server issue causing incomplete data.
else: # Promotion
engineer = entry['Engineer']
if 'Rank' in entry:
engineers[engineer] = (entry['Rank'], entry.get('RankProgress', 0))
if self.event_valid_engineerprogress(entry):
engineers = self.state['Engineers']
if 'Engineers' in entry: # Startup summary
self.state['Engineers'] = {
e['Engineer']: ((e['Rank'], e.get('RankProgress', 0)) if 'Rank' in e else e['Progress'])
for e in entry['Engineers']
}
else:
engineers[engineer] = entry['Progress']
else: # Promotion
engineer = entry['Engineer']
if 'Rank' in entry:
engineers[engineer] = (entry['Rank'], entry.get('RankProgress', 0))
else:
engineers[engineer] = entry['Progress']
elif event_type == 'Cargo' and entry.get('Vessel') == 'Ship':
self.state['Cargo'] = defaultdict(int)
@ -835,6 +840,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
{self.canonicalise(x['Name']): x['Count'] for x in clean_data}
)
# Journal v31 implies this was removed before Odyssey launch
elif event_type == 'BackPackMaterials':
# alpha4 -
# Lists the contents of the backpack, eg when disembarking from ship
@ -865,6 +871,80 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
{self.canonicalise(x['Name']): x['Count'] for x in clean_data}
)
elif event_type == 'BackPack':
# TODO: v31 doc says this is`backpack.json` ... but Howard Chalkley
# said it's `Backpack.json`
with open(join(self.currentdir, 'Backpack.json'), 'rb') as backpack: # type: ignore
try:
# Preserve property order because why not?
entry = json.load(backpack, object_pairs_hook=OrderedDict)
except json.JSONDecodeError:
logger.exception('Failed decoding Backpack.json', exc_info=True)
else:
# Store in monitor.state
self.state['BackpackJSON'] = entry
# Assume this reflects the current state when written
self.state['BackPack']['Component'] = defaultdict(int)
self.state['BackPack']['Consumable'] = defaultdict(int)
self.state['BackPack']['Item'] = defaultdict(int)
self.state['BackPack']['Data'] = defaultdict(int)
clean_components = self.coalesce_cargo(entry['Components'])
self.state['BackPack']['Component'].update(
{self.canonicalise(x['Name']): x['Count'] for x in clean_components}
)
clean_consumables = self.coalesce_cargo(entry['Consumables'])
self.state['BackPack']['Consumable'].update(
{self.canonicalise(x['Name']): x['Count'] for x in clean_consumables}
)
clean_items = self.coalesce_cargo(entry['Items'])
self.state['BackPack']['Item'].update(
{self.canonicalise(x['Name']): x['Count'] for x in clean_items}
)
clean_data = self.coalesce_cargo(entry['Data'])
self.state['BackPack']['Data'].update(
{self.canonicalise(x['Name']): x['Count'] for x in clean_data}
)
elif event_type == 'BackpackChange':
# Changes to Odyssey Backpack contents *other* than from a Transfer
# See TransferMicroResources event for that.
if entry.get('Added') is not None:
changes = 'Added'
elif entry.get('Removed') is not None:
changes = 'Removed'
else:
logger.warning(f'BackpackChange with neither Added nor Removed: {entry=}')
changes = ''
if changes != '':
for c in entry[changes]:
category = self.category(c['Type'])
name = self.canonicalise(c['Name'])
if changes == 'Removed':
self.state['BackPack'][category][name] -= c['Count']
elif changes == 'Added':
self.state['BackPack'][category][name] += c['Count']
# Paranoia check to see if anything has gone negative.
# As of Odyssey Alpha Phase 1 Hotfix 2 keeping track of BackPack
# materials is impossible when used/picked up anyway.
for c in self.state['BackPack']:
for m in self.state['BackPack'][c]:
if self.state['BackPack'][c][m] < 0:
self.state['BackPack'][c][m] = 0
elif event_type == 'BuyMicroResources':
# Buying from a Pioneer Supplies, goes directly to ShipLocker.
# One event per Item, not an array.
@ -948,80 +1028,70 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
self.state['BackPack'][entry['Type']][i] = 0
elif event_type == 'UseConsumable':
# alpha4
# When using an item from the players inventory (backpack)
# TODO: XXX: From v31 doc
# 12.2 BackpackChange
# This is written when there is any change to the contents of the
# suit backpack note this can be written at the same time as other
# events like UseConsumable
# In 4.0.0.100 it is observed that:
#
# Parameters:
# • Name
# • Type
for c in self.state['BackPack']['Consumable']:
if c == entry['Name']:
self.state['BackPack']['Consumable'][c] -= 1
# Paranoia in case we lost track
if self.state['BackPack']['Consumable'][c] < 0:
self.state['BackPack']['Consumable'][c] = 0
# 1. Throw of any grenade type *only* causes a BackpackChange event, no
# accompanying 'UseConsumable'.
# 2. Using an Energy Cell causes both UseConsumable and BackpackChange,
# in that order.
# 3. Medkit acts the same as Energy Cell.
#
# Thus we'll just ignore 'UseConsumable' for now.
# for c in self.state['BackPack']['Consumable']:
# if c == entry['Name']:
# self.state['BackPack']['Consumable'][c] -= 1
# # Paranoia in case we lost track
# if self.state['BackPack']['Consumable'][c] < 0:
# self.state['BackPack']['Consumable'][c] = 0
pass
# TODO:
# <https://forums.frontier.co.uk/threads/575010/>
# also there's one additional journal event that was missed out from
# this version of the docs: "SuitLoadout": # when starting on foot, or
# when disembarking from a ship, with the same info as found in "CreateSuitLoadout"
elif event_type == 'SuitLoadout':
suit_slotid, suitloadout_slotid = self.suitloadout_store_from_event(entry)
if not self.suit_and_loadout_setcurrent(suit_slotid, suitloadout_slotid):
logger.error(f"Event was: {entry}")
elif event_type == 'SwitchSuitLoadout':
loadoutid = entry['LoadoutID']
new_slot = self.suit_loadout_id_from_loadoutid(loadoutid)
# If this application is run with the latest Journal showing such an event then we won't
# yet have the CAPI data, so no idea about Suits or Loadouts.
if self.state['Suits'] and self.state['SuitLoadouts']:
try:
self.state['SuitLoadoutCurrent'] = self.state['SuitLoadouts'][f'{new_slot}']
except KeyError:
logger.debug(f"KeyError getting suit loadout after switch, bad slot: {new_slot} ({loadoutid})")
self.state['SuitCurrent'] = None
self.state['SuitLoadoutCurrent'] = None
else:
try:
new_suitid = self.state['SuitLoadoutCurrent']['suit']['suitId']
except KeyError:
logger.debug(f"KeyError getting switched-to suit ID from slot {new_slot} ({loadoutid})")
else:
try:
self.state['SuitCurrent'] = self.state['Suits'][f'{new_suitid}']
except KeyError:
logger.debug(f"KeyError getting switched-to suit from slot {new_slot} ({loadoutid}")
# 4.0.0.101
#
# { "timestamp":"2021-05-21T10:39:43Z", "event":"SwitchSuitLoadout",
# "SuitID":1700217809818876, "SuitName":"utilitysuit_class1",
# "SuitName_Localised":"Maverick Suit", "LoadoutID":4293000002,
# "LoadoutName":"K/P", "Modules":[ { "SlotName":"PrimaryWeapon1",
# "SuitModuleID":1700217863661544,
# "ModuleName":"wpn_m_assaultrifle_kinetic_fauto",
# "ModuleName_Localised":"Karma AR-50" },
# { "SlotName":"SecondaryWeapon", "SuitModuleID":1700216180036986,
# "ModuleName":"wpn_s_pistol_plasma_charged",
# "ModuleName_Localised":"Manticore Tormentor" } ] }
#
suit_slotid, suitloadout_slotid = self.suitloadout_store_from_event(entry)
if not self.suit_and_loadout_setcurrent(suit_slotid, suitloadout_slotid):
logger.error(f"Event was: {entry}")
elif event_type == 'CreateSuitLoadout':
# We know we won't have data for this new one
# Parameters:
# • SuitID
# • SuitName
# • LoadoutID
# • LoadoutName
# alpha4:
# { "timestamp":"2021-04-29T09:37:08Z", "event":"CreateSuitLoadout", "SuitID":1698364940285172,
# "SuitName":"tacticalsuit_class1", "SuitName_Localised":"Dominator Suit", "LoadoutID":4293000001,
# "LoadoutName":"Dom L/K/K", "Modules":[
# {
# "SlotName":"PrimaryWeapon1",
# "SuitModuleID":1698364962722310,
# "ModuleName":"wpn_m_assaultrifle_laser_fauto",
# "ModuleName_Localised":"TK Aphelion"
# },
# { "SlotName":"PrimaryWeapon2",
# "SuitModuleID":1698364956302993, "ModuleName":"wpn_m_assaultrifle_kinetic_fauto",
# "ModuleName_Localised":"Karma AR-50" }, { "SlotName":"SecondaryWeapon",
# "SuitModuleID":1698292655291850, "ModuleName":"wpn_s_pistol_kinetic_sauto",
# "ModuleName_Localised":"Karma P-15" } ] }
new_loadout = {
'loadoutSlotId': self.suit_loadout_id_from_loadoutid(entry['LoadoutID']),
'suit': {
'name': entry['SuitName'],
'locName': entry.get('SuitName_Localised', entry['SuitName']),
'suitId': entry['SuitID'],
},
'name': entry['LoadoutName'],
'slots': self.suit_loadout_slots_array_to_dict(entry['Modules']),
}
self.state['SuitLoadouts'][new_loadout['loadoutSlotId']] = new_loadout
# 4.0.0.101
#
# { "timestamp":"2021-05-21T11:13:15Z", "event":"CreateSuitLoadout", "SuitID":1700216165682989,
# "SuitName":"tacticalsuit_class1", "SuitName_Localised":"Dominator Suit", "LoadoutID":4293000004,
# "LoadoutName":"P/P/K", "Modules":[ { "SlotName":"PrimaryWeapon1", "SuitModuleID":1700216182854765,
# "ModuleName":"wpn_m_assaultrifle_plasma_fauto", "ModuleName_Localised":"Manticore Oppressor" },
# { "SlotName":"PrimaryWeapon2", "SuitModuleID":1700216190363340,
# "ModuleName":"wpn_m_shotgun_plasma_doublebarrel", "ModuleName_Localised":"Manticore Intimidator" },
# { "SlotName":"SecondaryWeapon", "SuitModuleID":1700217869872834,
# "ModuleName":"wpn_s_pistol_kinetic_sauto", "ModuleName_Localised":"Karma P-15" } ] }
#
_, _ = self.suitloadout_store_from_event(entry)
elif event_type == 'DeleteSuitLoadout':
# alpha4:
@ -1113,10 +1183,8 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
# • SuitID
# • Class
# • Cost
# Update credits total ? It shouldn't even involve credits!
# Actual alpha4 - need to grind mats
# if self.state['Suits']:
pass
# TODO: Update self.state['Suits'] when we have an example to work from
self.state['Credits'] -= entry.get('Cost', 0)
elif event_type == 'LoadoutEquipModule':
# alpha4:
@ -1513,6 +1581,120 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
logger.debug(f'Invalid journal entry:\n{line!r}\n', exc_info=ex)
return {'event': None}
def suitloadout_store_from_event(self, entry) -> Tuple[int, int]:
"""
Store Suit and SuitLoadout data from a journal event.
Also use set currently in-use instances of them as being as per this
event.
:param entry: Journal entry - 'SwitchSuitLoadout' or 'SuitLoadout'
:return Tuple[suit_slotid, suitloadout_slotid]: The IDs we set data for.
"""
suit_slotid = self.suit_loadout_id_from_loadoutid(entry['LoadoutID'])
# Initial suit containing just the data that is then embedded in
# the loadout
new_suit = {
'name': entry['SuitName'],
'locName': entry.get('SuitName_Localised', entry['SuitName']),
'suitId': entry['SuitID'],
}
# Make the new loadout, in the CAPI format
new_loadout = {
'loadoutSlotId': suit_slotid,
'suit': new_suit,
'name': entry['LoadoutName'],
'slots': self.suit_loadout_slots_array_to_dict(
entry['Modules']),
}
# Assign this loadout into our state
self.state['SuitLoadouts'][f"{new_loadout['loadoutSlotId']}"] = new_loadout
# Now add in the extra fields for new_suit to be a 'full' Suit structure
new_suit['id'] = None # Not available in 4.0.0.100 journal event
new_suit['slots'] = new_loadout['slots'] # 'slots', not 'Modules', to match CAPI
# Ensure new_suit is in self.state['Suits']
self.state['Suits'][f"{suit_slotid}"] = new_suit
return suit_slotid, new_loadout['loadoutSlotId']
def suit_and_loadout_setcurrent(self, suit_slotid: int, suitloadout_slotid: int) -> bool:
"""
Set self.state for SuitCurrent and SuitLoadoutCurrent as requested.
If the specified slots are unknown we abort and return False, else
return True.
:param suit_slotid: Numeric ID of the slot for the suit.
:param suitloadout_slotid: Numeric ID of the slot for the suit loadout.
:return: True if we could do this, False if not.
"""
str_suitid = f"{suit_slotid}"
str_suitloadoutid = f"{suitloadout_slotid}"
if (self.state['Suits'].get(str_suitid, False)
and self.state['SuitLoadouts'].get(str_suitloadoutid, False)):
self.state['SuitCurrent'] = self.state['Suits'][str_suitid]
self.state['SuitLoadoutCurrent'] = self.state['SuitLoadouts'][str_suitloadoutid]
return True
logger.error(f"Tried to set a suit and suitloadout where we didn't know about both: {suit_slotid=}, "
f"{str_suitloadoutid=}")
return False
# TODO: *This* will need refactoring and a proper validation infrastructure
# designed for this in the future. This is a bandaid for a known issue.
def event_valid_engineerprogress(self, entry) -> bool: # noqa: CCR001 C901
"""
Check an `EngineerProgress` Journal event for validity.
:param entry: Journal event dict
:return: True if passes validation, else False.
"""
# The event should have at least one of thes
if 'Engineers' not in entry and 'Progress' not in entry:
logger.warning(f"EngineerProgress has neither 'Engineers' nor 'Progress': {entry=}")
return False
# But not both of them
if 'Engineers' in entry and 'Progress' in entry:
logger.warning(f"EngineerProgress has BOTH 'Engineers' and 'Progress': {entry=}")
return False
if 'Engineers' in entry:
# 'Engineers' version should have a list as value
if not isinstance(entry['Engineers'], list):
logger.warning(f"EngineerProgress 'Engineers' is not a list: {entry=}")
return False
# It should have at least one entry? This might still be valid ?
if len(entry['Engineers']) < 1:
logger.warning(f"EngineerProgress 'Engineers' list is empty ?: {entry=}")
# TODO: As this might be valid, we might want to only log
return False
# And that list should have all of these keys
for e in entry['Engineers']:
for f in ('Engineer', 'EngineerID', 'Rank', 'Progress', 'RankProgress'):
if f not in e:
# For some Progress there's no Rank/RankProgress yet
if f in ('Rank', 'RankProgress'):
if (progress := e.get('Progress', None)) is not None:
if progress in ('Invited', 'Known'):
continue
logger.warning(f"Engineer entry without '{f}' key: {e=} in {entry=}")
return False
if 'Progress' in entry:
for e in entry['Engineers']:
for f in ('Engineer', 'EngineerID', 'Rank', 'Progress', 'RankProgress'):
if f not in e:
logger.warning(f"Engineer entry without '{f}' key: {e=} in {entry=}")
return False
return True
def suit_loadout_id_from_loadoutid(self, journal_loadoutid: int) -> int:
"""
Determine the CAPI-oriented numeric slot id for a Suit Loadout.

View File

@ -432,6 +432,11 @@ def journal_entry(
elif entry['event'] == 'NavBeaconScan':
this.navbeaconscan = entry['NumBodies']
elif entry['event'] == 'BackPack':
# Use the stored file contents, not the empty journal event
if state['BackpackJSON']:
entry = state['BackpackJSON']
# Send interesting events to EDSM
if (
config.get_int('edsm_out') and not is_beta and not this.multicrew and credentials(cmdr) and

View File

@ -449,14 +449,17 @@ def journal_entry( # noqa: C901, CCR001
)
# Update location
new_add_event(
'setCommanderTravelLocation',
entry['timestamp'],
OrderedDict([
('starsystemName', system),
('stationName', station), # Can be None
])
)
# Might not be available if this event is a 'StartUp' and we're replaying
# a log.
if system:
new_add_event(
'setCommanderTravelLocation',
entry['timestamp'],
OrderedDict([
('starsystemName', system),
('stationName', station), # Can be None
])
)
# Update ship
if state['ShipID']: # Unknown if started in Fighter or SRV

View File

@ -535,10 +535,9 @@ class PreferencesDialog(tk.Toplevel):
)
with row as cur_row:
shipyard_provider = config.get_str('shipyard_provider')
self.shipyard_provider = tk.StringVar(
value=str(
config.get_str('shipyard_provider') in plug.provides('shipyard_url')
and config.get_str('shipyard_provider', default='EDSY'))
value=str(shipyard_provider if shipyard_provider in plug.provides('shipyard_url') else 'EDSY')
)
# Setting to decide which ship outfitting website to link to - either E:D Shipyard or Coriolis
nb.Label(config_frame, text=_('Shipyard')).grid(padx=self.PADX, pady=2*self.PADY, sticky=tk.W, row=cur_row)

View File

@ -1,6 +1,6 @@
certifi==2020.12.5
requests==2.25.1
watchdog==2.1.1
watchdog==2.1.2
# Commented out because this doesn't package well with py2exe
# infi.systray==0.1.12; sys_platform == 'win32'
# argh==0.26.2 watchdog dep

8
td.py
View File

@ -25,7 +25,15 @@ def export(data):
querytime = config.get_int('querytime', default=int(time.time()))
#
# When this is refactored into multi-line CHECK IT WORKS, avoiding the
# brainfart we had with dangling commas in commodity.py:export() !!!
#
filename = join(config.get_str('outdir'), '%s.%s.%s.prices' % (data['lastSystem']['name'].strip(), data['lastStarport']['name'].strip(), time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime))))
#
# When this is refactored into multi-line CHECK IT WORKS, avoiding the
# brainfart we had with dangling commas in commodity.py:export() !!!
#
timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.strptime(data['timestamp'], '%Y-%m-%dT%H:%M:%SZ'))