Skip to content

Commit

Permalink
Merge branch 'feature/output-formatters' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
aussig committed Apr 27, 2024
2 parents 02d56cb + 17afafc commit 84f35f6
Show file tree
Hide file tree
Showing 16 changed files with 1,153 additions and 591 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* Enemy captain kills 👨‍✈️ will sometimes be tallied and sometimes not, for the same reason.
* Enemy propagandist wing kills ✒️ are also tallied as soon as BGS-Tally spots the **first detectable kill** in the propagandist wing, for the same reason.
* Added tooltips (hover text) to all abbreviations on screen, and a few of the controls and buttons that are not self-explanatory.
* Added support for different Discord post formats. So if your squadron or group would like your discord activity posts to look different, this can be done. Currently it's a programming job to create a new format (so ask your friendly Python developer to get in touch, or send in a suggestion for a new format to the BGS-Tally Discord server).

### Changes:

Expand Down
556 changes: 118 additions & 438 deletions bgstally/activity.py

Large diffs are not rendered by default.

42 changes: 24 additions & 18 deletions bgstally/bgstally.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from bgstally.debug import Debug
from bgstally.discord import Discord
from bgstally.fleetcarrier import FleetCarrier
from bgstally.formatters.default import DefaultActivityFormatter
from bgstally.formattermanager import ActivityFormatterManager
from bgstally.market import Market
from bgstally.missionlog import MissionLog
from bgstally.overlay import Overlay
Expand Down Expand Up @@ -47,14 +49,18 @@ def plugin_start(self, plugin_dir: str):
self.plugin_dir = plugin_dir

# Debug and Config Classes
self.debug:Debug = Debug(self)
self.config:Config = Config(self)
self.debug: Debug = Debug(self)
self.config: Config = Config(self)

# True only if we are running a dev version
self.dev_mode: bool = False

# Load sentry to track errors during development - Hard check on "dev" versions ONLY (which never go out to testers)
# If you are a developer and want to use sentry, install the sentry_sdk inside the ./thirdparty folder and add your full dsn
# (starting https://) to a 'sentry' entry in config.ini file. Set the plugin version in load.py to include a 'dev' prerelease,
# e.g. "3.3.0-dev"
if type(self.version.prerelease) is tuple and len(self.version.prerelease) > 0 and self.version.prerelease[0] == "dev":
self.dev_mode = True
sys.path.append(path.join(plugin_dir, 'thirdparty'))
try:
import sentry_sdk
Expand All @@ -69,22 +75,22 @@ def plugin_start(self, plugin_dir: str):
if not path.exists(data_filepath): mkdir(data_filepath)

# Main Classes
self.state:State = State(self)
self.mission_log:MissionLog = MissionLog(self)
self.target_log:TargetLog = TargetLog(self)
self.discord:Discord = Discord(self)
self.tick:Tick = Tick(self, True)
self.overlay:Overlay = Overlay(self)
self.activity_manager:ActivityManager = ActivityManager(self)
self.fleet_carrier:FleetCarrier = FleetCarrier(self)
self.market:Market = Market(self)
self.request_manager:RequestManager = RequestManager(self)
self.api_manager:APIManager = APIManager(self)
self.webhook_manager:WebhookManager = WebhookManager(self)
self.update_manager:UpdateManager = UpdateManager(self)
self.ui:UI = UI(self)

self.thread:Thread = Thread(target=self._worker, name="BGSTally Main worker")
self.state: State = State(self)
self.mission_log: MissionLog = MissionLog(self)
self.target_log: TargetLog = TargetLog(self)
self.discord: Discord = Discord(self)
self.tick: Tick = Tick(self, True)
self.overlay: Overlay = Overlay(self)
self.activity_manager: ActivityManager = ActivityManager(self)
self.fleet_carrier: FleetCarrier = FleetCarrier(self)
self.market: Market = Market(self)
self.request_manager: RequestManager = RequestManager(self)
self.api_manager: APIManager = APIManager(self)
self.webhook_manager: WebhookManager = WebhookManager(self)
self.update_manager: UpdateManager = UpdateManager(self)
self.ui: UI = UI(self)
self.formatter_manager: ActivityFormatterManager = ActivityFormatterManager(self)
self.thread: Thread = Thread(target=self._worker, name="BGSTally Main worker")
self.thread.daemon = True
self.thread.start()

Expand Down
8 changes: 4 additions & 4 deletions bgstally/discord.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def post_plaintext(self, discord_text:str, webhooks_data:dict|None, channel:Disc
# Get the previous state for this webhook's uuid from the passed in data, if it exists. Default to the state from the webhook manager
specific_webhook_data:dict = {} if webhooks_data is None else webhooks_data.get(webhook.get('uuid', ""), webhook)

utc_time_now:str = datetime.utcnow().strftime(DATETIME_FORMAT) + " " + __("game") # LANG: Discord date/time suffix for game time
utc_time_now:str = datetime.utcnow().strftime(DATETIME_FORMAT) + " " + __("game", lang=self.bgstally.state.discord_lang) # LANG: Discord date/time suffix for game time
data:dict = {'channel': channel, 'callback': callback, 'webhookdata': specific_webhook_data} # Data that's carried through the request queue and back to the callback

# Fetch the previous post ID, if present, from the webhook data for the channel we're posting in. May be the default True / False value
Expand All @@ -46,15 +46,15 @@ def post_plaintext(self, discord_text:str, webhooks_data:dict|None, channel:Disc
# No previous post
if discord_text == "": return

discord_text += ("```ansi\n" + blue(__("Posted at: {date_time} | {plugin_name} v{version}")) + "```").format(date_time=utc_time_now, plugin_name=self.bgstally.plugin_name, version=str(self.bgstally.version)) # LANG: Discord message footer, legacy text mode
discord_text += ("```ansi\n" + blue(__("Posted at: {date_time} | {plugin_name} v{version}", lang=self.bgstally.state.discord_lang)) + "```").format(date_time=utc_time_now, plugin_name=self.bgstally.plugin_name, version=str(self.bgstally.version)) # LANG: Discord message footer, legacy text mode
url:str = webhook_url
payload:dict = {'content': discord_text, 'username': self.bgstally.state.DiscordUsername.get(), 'embeds': []}

self.bgstally.request_manager.queue_request(url, RequestMethod.POST, payload=payload, callback=self._request_complete, data=data)
else:
# Previous post
if discord_text != "":
discord_text += ("```ansi\n" + green(__("Updated at: {date_time} | {plugin_name} v{version}")) + "```").format(date_time=utc_time_now, plugin_name=self.bgstally.plugin_name, version=str(self.bgstally.version)) # LANG: Discord message footer, legacy text mode
discord_text += ("```ansi\n" + green(__("Updated at: {date_time} | {plugin_name} v{version}", lang=self.bgstally.state.discord_lang)) + "```").format(date_time=utc_time_now, plugin_name=self.bgstally.plugin_name, version=str(self.bgstally.version)) # LANG: Discord message footer, legacy text mode
url:str = f"{webhook_url}/messages/{previous_messageid}"
payload:dict = {'content': discord_text, 'username': self.bgstally.state.DiscordUsername.get(), 'embeds': []}

Expand Down Expand Up @@ -144,7 +144,7 @@ def _get_embed(self, title:str, description:str, fields:list, update:bool) -> di
Create a Discord embed JSON structure. If supplied, `fields` should be a List of Dicts, with each Dict containing 'name' (the field title) and
'value' (the field contents)
"""
footer_timestamp:str = (__("Updated at {date_time} (game)") if update else __("Posted at {date_time} (game)")).format(date_time=datetime.utcnow().strftime(DATETIME_FORMAT)) # LANG: Discord footer message, modern embed mode
footer_timestamp:str = (__("Updated at {date_time} (game)", lang=self.bgstally.state.discord_lang) if update else __("Posted at {date_time} (game)", lang=self.bgstally.state.discord_lang)).format(date_time=datetime.utcnow().strftime(DATETIME_FORMAT)) # LANG: Discord footer message, modern embed mode
footer_version:str = f"{self.bgstally.plugin_name} v{str(self.bgstally.version)}"
footer_pad:int = 108 - len(footer_version)

Expand Down
38 changes: 19 additions & 19 deletions bgstally/fleetcarrier.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,16 @@ def jump_requested(self, journal_entry: dict):
"""
# {"timestamp": "2020-04-20T09:30:58Z", "event": "CarrierJumpRequest", "CarrierID": 3700005632, "SystemName": "Paesui Xena", "Body": "Paesui Xena A", "SystemAddress": 7269634680241, "BodyID": 1, "DepartureTime":"2020-04-20T09:45:00Z"}

title:str = __("Jump Scheduled for Carrier {carrier_name}").format(carrier_name=self.name) # LANG: Discord post title
description:str = __("A carrier jump has been scheduled") # LANG: Discord text
title:str = __("Jump Scheduled for Carrier {carrier_name}", lang=self.bgstally.state.discord_lang).format(carrier_name=self.name) # LANG: Discord post title
description:str = __("A carrier jump has been scheduled", lang=self.bgstally.state.discord_lang) # LANG: Discord text

fields = []
fields.append({'name': __("From System"), 'value': self.data.get('currentStarSystem', "Unknown"), 'inline': True}) # LANG: Discord heading
fields.append({'name': __("To System"), 'value': journal_entry.get('SystemName', "Unknown"), 'inline': True}) # LANG: Discord heading
fields.append({'name': __("To Body"), 'value': journal_entry.get('Body', "Unknown"), 'inline': True}) # LANG: Discord heading
fields.append({'name': __("Departure Time"), 'value': datetime.strptime(journal_entry.get('DepartureTime'), DATETIME_FORMAT_JOURNAL).strftime(DATETIME_FORMAT), 'inline': True}) # LANG: Discord heading
fields.append({'name': __("Docking"), 'value': self.human_format_dockingaccess(True), 'inline': True}) # LANG: Discord heading
fields.append({'name': __("Notorious Access"), 'value': self.human_format_notorious(True), 'inline': True}) # LANG: Discord heading
fields.append({'name': __("From System", lang=self.bgstally.state.discord_lang), 'value': self.data.get('currentStarSystem', "Unknown"), 'inline': True}) # LANG: Discord heading
fields.append({'name': __("To System", lang=self.bgstally.state.discord_lang), 'value': journal_entry.get('SystemName', "Unknown"), 'inline': True}) # LANG: Discord heading
fields.append({'name': __("To Body", lang=self.bgstally.state.discord_lang), 'value': journal_entry.get('Body', "Unknown"), 'inline': True}) # LANG: Discord heading
fields.append({'name': __("Departure Time", lang=self.bgstally.state.discord_lang), 'value': datetime.strptime(journal_entry.get('DepartureTime'), DATETIME_FORMAT_JOURNAL).strftime(DATETIME_FORMAT), 'inline': True}) # LANG: Discord heading
fields.append({'name': __("Docking", lang=self.bgstally.state.discord_lang), 'value': self.human_format_dockingaccess(True), 'inline': True}) # LANG: Discord heading
fields.append({'name': __("Notorious Access", lang=self.bgstally.state.discord_lang), 'value': self.human_format_notorious(True), 'inline': True}) # LANG: Discord heading

self.bgstally.discord.post_embed(title, description, fields, None, DiscordChannel.FLEETCARRIER_OPERATIONS, None)

Expand All @@ -135,13 +135,13 @@ def jump_cancelled(self):
"""
The user cancelled their carrier jump
"""
title:str = __("Jump Cancelled for Carrier {carrier_name}").format(carrier_name=self.name) # LANG: Discord post title
description:str = __("The scheduled carrier jump was cancelled") # LANG: Discord text
title:str = __("Jump Cancelled for Carrier {carrier_name}", lang=self.bgstally.state.discord_lang).format(carrier_name=self.name) # LANG: Discord post title
description:str = __("The scheduled carrier jump was cancelled", lang=self.bgstally.state.discord_lang) # LANG: Discord text

fields = []
fields.append({'name': __("Current System"), 'value': self.data.get('currentStarSystem', "Unknown"), 'inline': True})
fields.append({'name': __("Docking"), 'value': self.human_format_dockingaccess(True), 'inline': True})
fields.append({'name': __("Notorious Access"), 'value': self.human_format_notorious(True), 'inline': True})
fields.append({'name': __("Current System", lang=self.bgstally.state.discord_lang), 'value': self.data.get('currentStarSystem', "Unknown"), 'inline': True})
fields.append({'name': __("Docking", lang=self.bgstally.state.discord_lang), 'value': self.human_format_dockingaccess(True), 'inline': True})
fields.append({'name': __("Notorious Access", lang=self.bgstally.state.discord_lang), 'value': self.human_format_notorious(True), 'inline': True})

self.bgstally.discord.post_embed(title, description, fields, None, DiscordChannel.FLEETCARRIER_OPERATIONS, None)

Expand Down Expand Up @@ -271,20 +271,20 @@ def human_format_dockingaccess(self, discord:bool) -> str:
Get the docking access in human-readable format
"""
match (self.data.get('dockingAccess')):
case "all": return __("All") if discord else _("All") # LANG: Discord carrier docking access
case "squadronfriends": return __("Squadron and Friends") if discord else _("Squadron and Friends") # LANG: Discord carrier docking access
case "friends": return __("Friends") if discord else _("Friends") # LANG: Discord carrier docking access
case _: return __("None") if discord else _("None") # LANG: Discord carrier docking access
case "all": return __("All", lang=self.bgstally.state.discord_lang) if discord else _("All") # LANG: Discord carrier docking access
case "squadronfriends": return __("Squadron and Friends", lang=self.bgstally.state.discord_lang) if discord else _("Squadron and Friends") # LANG: Discord carrier docking access
case "friends": return __("Friends", lang=self.bgstally.state.discord_lang) if discord else _("Friends") # LANG: Discord carrier docking access
case _: return __("None", lang=self.bgstally.state.discord_lang) if discord else _("None") # LANG: Discord carrier docking access


def human_format_notorious(self, discord:bool) -> str:
"""
Get the notorious access in human-readable format
"""
if self.data.get('notoriousAccess', False):
return __("Yes") if discord else _("Yes")
return __("Yes", lang=self.bgstally.state.discord_lang) if discord else _("Yes")
else:
return __("No") if discord else _("No")
return __("No", lang=self.bgstally.state.discord_lang) if discord else _("No")


def _human_format_price(self, num) -> str:
Expand Down
73 changes: 73 additions & 0 deletions bgstally/formattermanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from importlib import import_module
from os import listdir, path

from bgstally.debug import Debug
from bgstally.formatters.base import BaseActivityFormatterInterface, FieldActivityFormatterInterface, TextActivityFormatterInterface
from bgstally.formatters.default import DefaultActivityFormatter
from bgstally.utils import all_subclasses

for module in listdir(path.join(path.dirname(__file__), "formatters")):
if module[-3:] == '.py': import_module("bgstally.formatters." + module[:-3])
del module


class ActivityFormatterManager:
"""
Handles the management of output formatters
"""

def __init__(self, bgstally):
self.bgstally = bgstally

# Create list of instances of each subclass of FormatterInterface
self._formatters: dict[str: BaseActivityFormatterInterface] = {}

for cls in all_subclasses(FieldActivityFormatterInterface) | all_subclasses(TextActivityFormatterInterface):
instance: BaseActivityFormatterInterface = cls(bgstally)
self._formatters[cls.__name__] = instance

Debug.logger.info(f"formatters: {self._formatters}")


def get_formatters(self) -> dict[str: str]:
"""Get the available formatters
Returns:
dict: key = formatter class name, value = formatter public name
"""
result: dict = {}

for class_name, class_instance in self._formatters.items():
if class_instance.is_visible(): result[class_name] = class_instance.get_name()

return result


def get_formatter(self, class_name: str) -> BaseActivityFormatterInterface | None:
"""Get a specific formatter by its class name
Args:
name (str): The class name of the formatter
Returns:
BaseFormatterInterface | None: The formatter
"""
return self._formatters.get(class_name)


def get_default_formatter(self) -> DefaultActivityFormatter | None:
"""Get the default formatter
Returns:
DefaultFormatter | None: The formatter
"""
return self.get_formatter(DefaultActivityFormatter.__name__)


def get_current_formatter(self) -> BaseActivityFormatterInterface:
"""Get the currently selected activity formatter
Returns:
BaseActivityFormatterInterface: Activity Formatter
"""
return self.get_formatter(self.bgstally.state.discord_formatter)
Loading

0 comments on commit 84f35f6

Please sign in to comment.