Skip to content

Commit

Permalink
Merge branch 'feature/issue-291' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
aussig committed Jan 25, 2025
2 parents 8109d1f + 91eac63 commit 0d31e96
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 17 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

### New Features:

* Objectives! If you use the API to connect to a server that supports them (API ≥ v1.6.0) then your squadron or group can define shared missions that multiple CMDRs can work towards. Missions can be of various types (for example - `win a war` or `boost a faction`) and each mission can have one or more targets (for example - `win xx space CZs` or `generate yyy CR in trade profit`). The objectives are shown on the overlay in-game if you have it enabled.
* Objectives! If you use the API to connect to a server that supports them (API ≥ v1.6.0) then your squadron or group can define shared missions that multiple CMDRs can work towards.
- Missions can be of various types (for example - `win a war` or `boost a faction`) and each mission can have one or more targets (for example - `win xx space CZs` or `generate yyy CR in trade profit`).
- Objectives are shown in a new window accessible from the main EDMC window - click the 𖦏 button. The layout is a bit basic at the moment, it will probably improve in future.
- If you use the in-game overlay, Objectives are also displayed on a new overlay panel in-game.
* Conflict states are highlighted in the activity window: Elections in orange and wars in red.
* The individual tick time for each system is now reported on the activity window and on the overlay in-game.

Expand Down
Binary file added assets/button_objectives.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion bgstally/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ def __init__(self, bgstally, data: list = None):
# Events queue is used to batch up events API messages. All batched messages are sent when the worker works.
self.events_queue: Queue = Queue()

# Received data state (transient, we don't save or load this)
self.objectives: list = []

self.activities_thread: Thread = Thread(target=self._activities_worker, name=f"BGSTally Activities API Worker ({self.url})")
self.activities_thread.daemon = True
self.activities_thread.start()
Expand Down Expand Up @@ -338,7 +341,8 @@ def _objectives_received(self, success: bool, response: Response, request: BGSTa
Debug.logger.warning(f"Objectives data is invalid (not a list)")
return

self.bgstally.objectives_manager.set_objectives(objectives_data)
self.objectives = objectives_data
self.bgstally.objectives_manager.objectives_received(self)


def _get_headers(self) -> dict:
Expand Down
36 changes: 28 additions & 8 deletions bgstally/objectivesmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,25 @@ class MissionTargetType(str, Enum):

class ObjectivesManager:
"""
Handles the management of objectives
Handles the management of objectives.
Note that the objectives are stored inside the API object and there is only a single API tracked here. So, if multiple APIs
are implemented in future, it will flip-flop between them and we either need to handle that or limit objectives to a single API.
"""
def __init__(self, bgstally):
self.bgstally = bgstally

self._objectives: list[dict] = []
self.api: API = None


def objectives_available(self) -> bool:
"""Check whether any objectives are available
Returns:
bool: True if there are objectives
"""
if self.api is None: return False
else: return len(self.api.objectives) > 0


def get_objectives(self) -> list:
Expand All @@ -47,16 +60,22 @@ def get_objectives(self) -> list:
Returns:
list: current list of objectives
"""
return self._objectives
if self.api is None: return []
else: return self.api.objectives


def set_objectives(self, objectives: list):
"""Set the current objectives
def objectives_received(self, api: API):
"""Objectives have been received from the API
Args:
objectives (dict): The list of objectives
api (API): The API object
"""
self._objectives = objectives
previous_available: bool = self.objectives_available()
self.api = api

if previous_available != self.objectives_available():
# We've flipped from having objectives to not having objectives or vice versa. Refresh the plugin frame.
self.bgstally.ui.frame.after(1000, self.bgstally.ui.update_plugin_frame())


def get_human_readable_objectives(self) -> str:
Expand All @@ -66,8 +85,9 @@ def get_human_readable_objectives(self) -> str:
str: The human readable objectives
"""
result: str = ""
if self.api is None: return result

for mission in self._objectives:
for mission in self.api.objectives:
mission_title: str|None = mission.get('title')
mission_description: str|None = mission.get('description')
mission_system: str|None = mission.get('system')
Expand Down
39 changes: 32 additions & 7 deletions bgstally/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from bgstally.windows.cmdrs import WindowCMDRs
from bgstally.windows.fleetcarrier import WindowFleetCarrier
from bgstally.windows.legend import WindowLegend
from bgstally.windows.objectives import WindowObjectives
from config import config
from thirdparty.tksheet import Sheet
from thirdparty.Tooltip import ToolTip
Expand Down Expand Up @@ -50,6 +51,7 @@ def __init__(self, bgstally):
self.image_button_dropdown_menu = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "button_dropdown_menu.png"))
self.image_button_cmdrs = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "button_cmdrs.png"))
self.image_button_carrier = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "button_carrier.png"))
self.image_button_objectives = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "button_objectives.png"))
self.image_icon_green_tick = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "icon_green_tick_16x16.png"))
self.image_icon_red_cross = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "icon_red_cross_16x16.png"))

Expand All @@ -62,6 +64,8 @@ def __init__(self, bgstally):
self.window_cmdrs:WindowCMDRs = WindowCMDRs(self.bgstally)
self.window_fc:WindowFleetCarrier = WindowFleetCarrier(self.bgstally)
self.window_legend:WindowLegend = WindowLegend(self.bgstally)
self.window_objectives:WindowObjectives = WindowObjectives(self.bgstally)

# TODO: When we support multiple APIs, this will no longer be a single instance window
self.window_api:WindowAPI = WindowAPI(self.bgstally, self.bgstally.api_manager.apis[0])

Expand All @@ -85,34 +89,48 @@ def get_plugin_frame(self, parent_frame: tk.Frame) -> tk.Frame:
"""
self.frame: tk.Frame = tk.Frame(parent_frame)

column_count: int = 3
if self.bgstally.capi_fleetcarrier_available(): column_count += 1

current_row: int = 0
tk.Label(self.frame, image=self.image_logo_bgstally_100).grid(row=current_row, column=0, rowspan=3, sticky=tk.W)
self.lbl_version: HyperlinkLabel = HyperlinkLabel(self.frame, text=f"v{str(self.bgstally.version)}", background=nb.Label().cget('background'), url=URL_LATEST_RELEASE, underline=True)
self.lbl_version.grid(row=current_row, column=1, columnspan=3 if self.bgstally.capi_fleetcarrier_available() else 2, sticky=tk.W)
self.lbl_version.grid(row=current_row, column=1, columnspan=column_count, sticky=tk.W)
current_row += 1
frm_status: tk.Frame = tk.Frame(self.frame)
frm_status.grid(row=current_row, column=1, columnspan=3 if self.bgstally.capi_fleetcarrier_available() else 2, sticky=tk.W)
frm_status.grid(row=current_row, column=1, columnspan=column_count, sticky=tk.W)
self.lbl_status: tk.Label = tk.Label(frm_status, text=_("{plugin_name} Status:").format(plugin_name=self.bgstally.plugin_name)) # LANG: Main window label
self.lbl_status.pack(side=tk.LEFT)
self.lbl_active: tk.Label = tk.Label(frm_status, width=SIZE_STATUS_ICON_PIXELS, height=SIZE_STATUS_ICON_PIXELS, image=self.image_icon_green_tick if self.bgstally.state.Status.get() == CheckStates.STATE_ON else self.image_icon_red_cross)
self.lbl_active.pack(side=tk.LEFT)
current_row += 1
self.lbl_tick: tk.Label = tk.Label(self.frame, text=_("Last BGS Tick:") + " " + self.bgstally.tick.get_formatted()) # LANG: Main window label
self.lbl_tick.grid(row=current_row, column=1, columnspan=3 if self.bgstally.capi_fleetcarrier_available() else 2, sticky=tk.W)
self.lbl_tick.grid(row=current_row, column=1, columnspan=column_count, sticky=tk.W)
current_row += 1
current_column: int = 0
self.btn_latest_tick: tk.Button = tk.Button(self.frame, text=_("Latest BGS Tally"), height=SIZE_BUTTON_PIXELS-2, image=self.image_blank, compound=tk.RIGHT, command=partial(self._show_activity_window, self.bgstally.activity_manager.get_current_activity())) # LANG: Button label
self.btn_latest_tick.grid(row=current_row, column=0, padx=3)
self.btn_latest_tick.grid(row=current_row, column=current_column, padx=3)
current_column += 1
self.btn_previous_ticks: tk.Button = tk.Button(self.frame, text=_("Previous BGS Tallies") + " ", height=SIZE_BUTTON_PIXELS-2, image=self.image_button_dropdown_menu, compound=tk.RIGHT, command=self._previous_ticks_popup) # LANG: Button label
self.btn_previous_ticks.grid(row=current_row, column=1, padx=3, sticky=tk.W)
self.btn_previous_ticks.grid(row=current_row, column=current_column, padx=3, sticky=tk.W)
current_column += 1
self.btn_cmdrs: tk.Button = tk.Button(self.frame, image=self.image_button_cmdrs, height=SIZE_BUTTON_PIXELS, width=SIZE_BUTTON_PIXELS, command=self._show_cmdr_list_window)
self.btn_cmdrs.grid(row=current_row, column=2, padx=3)
self.btn_cmdrs.grid(row=current_row, column=current_column, padx=3)
current_column += 1
ToolTip(self.btn_cmdrs, text=_("Show CMDR information window")) # LANG: Main window tooltip
if self.bgstally.capi_fleetcarrier_available():
self.btn_carrier: tk.Button = tk.Button(self.frame, image=self.image_button_carrier, state=('normal' if self.bgstally.fleet_carrier.available() else 'disabled'), height=SIZE_BUTTON_PIXELS, width=SIZE_BUTTON_PIXELS, command=self._show_fc_window)
self.btn_carrier.grid(row=current_row, column=3, padx=3)
self.btn_carrier.grid(row=current_row, column=current_column, padx=3)
ToolTip(self.btn_carrier, text=_("Show fleet carrier window")) # LANG: Main window tooltip
current_column += 1
else:
self.btn_carrier: tk.Button = None

self.btn_objectives: tk.Button = tk.Button(self.frame, image=self.image_button_objectives, state=('normal' if self.bgstally.objectives_manager.objectives_available() else 'disabled'), height=SIZE_BUTTON_PIXELS, width=SIZE_BUTTON_PIXELS, command=self._show_objectives_window)
self.btn_objectives.grid(row=current_row, column=current_column, padx=3)
ToolTip(self.btn_objectives, text=_("Show objectives / missions window")) # LANG: Main window tooltip
current_column += 1

current_row += 1

return self.frame
Expand All @@ -137,6 +155,7 @@ def update_plugin_frame(self):
self.btn_latest_tick.config(command=partial(self._show_activity_window, self.bgstally.activity_manager.get_current_activity()))
if self.btn_carrier is not None:
self.btn_carrier.config(state=('normal' if self.bgstally.fleet_carrier.available() else 'disabled'))
self.btn_objectives.config(state=('normal' if self.bgstally.objectives_manager.objectives_available() else 'disabled'))


def get_prefs_frame(self, parent_frame: tk.Frame):
Expand Down Expand Up @@ -489,6 +508,12 @@ def _show_fc_window(self):
self.window_fc.show()


def _show_objectives_window(self):
"""Display the Objectives Window
"""
self.window_objectives.show()


def _show_api_window(self, parent_frame:tk.Frame):
"""
Display the API configuration window
Expand Down
66 changes: 66 additions & 0 deletions bgstally/windows/objectives.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import tkinter as tk
from tkinter import ttk

from bgstally.constants import COLOUR_HEADING_1, FONT_HEADING_1, FONT_TEXT
from bgstally.debug import Debug
from bgstally.utils import _, __
from bgstally.widgets import TextPlus
from config import config
from thirdparty.colors import *


class WindowObjectives:
"""
Handles the Objectives window
"""

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

self.toplevel: tk.Toplevel = None


def show(self):
"""
Show our window
"""
if self.toplevel is not None and self.toplevel.winfo_exists():
self.toplevel.lift()
return

self.toplevel = tk.Toplevel(self.bgstally.ui.frame)
self.toplevel.title(_("{plugin_name} - Objectives").format(plugin_name=self.bgstally.plugin_name, )) # LANG: Objectives window title
self.toplevel.iconphoto(False, self.bgstally.ui.image_logo_bgstally_32, self.bgstally.ui.image_logo_bgstally_16)
self.toplevel.geometry("800x800")

frm_container: ttk.Frame = ttk.Frame(self.toplevel)
frm_container.pack(fill=tk.BOTH, expand=True)

ttk.Label(frm_container, text=_("Objectives from {server_name}".format(server_name=self.bgstally.objectives_manager.api.name)), font=FONT_HEADING_1, foreground=COLOUR_HEADING_1).pack(anchor=tk.NW) # LANG: Label on objectives window

frm_items: ttk.Frame = ttk.Frame(frm_container)
frm_items.pack(fill=tk.BOTH, padx=5, pady=5, expand=True)

current_row: int = 0

self.txt_objectives: TextPlus = TextPlus(frm_items, wrap=tk.WORD, height=1, font=FONT_TEXT)
sb_objectives: tk.Scrollbar = tk.Scrollbar(frm_items, orient=tk.VERTICAL, command=self.txt_objectives.yview)
self.txt_objectives['yscrollcommand'] = sb_objectives.set
sb_objectives.pack(fill=tk.Y, side=tk.RIGHT)
self.txt_objectives.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)

self.txt_objectives.insert(tk.INSERT, self.bgstally.objectives_manager.get_human_readable_objectives())
self.txt_objectives.configure(state='disabled')

self.toplevel.after(5000, self._update_objectives)


def _update_objectives(self):
"""Refresh the objectives
"""
self.txt_objectives.configure(state=tk.NORMAL)
self.txt_objectives.delete('1.0', 'end-1c')
self.txt_objectives.insert(tk.INSERT, self.bgstally.objectives_manager.get_human_readable_objectives())
self.txt_objectives.configure(state=tk.DISABLED)

self.toplevel.after(5000, self._update_objectives)

0 comments on commit 0d31e96

Please sign in to comment.