From 04b4b96cae83c6ab8f2c8e39657f73a6e4521c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B2=E8=8F=AF?= <42814579+yunwah@users.noreply.github.com> Date: Sat, 1 May 2021 18:28:02 -0400 Subject: [PATCH] Initial commit The following components and features have been implemented per this initial commit: - Python library wrapping warframestat.us API endpoint w/ decorated functions - Full transparency for overlay window (click through and alpha) - Added status for open worlds - Added support for variable window sizes (does not resize automatically) --- src/overlay.py | 123 ++++++++++++++++++++++++++++++++++ src/warframe_api.py | 160 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 src/overlay.py create mode 100644 src/warframe_api.py diff --git a/src/overlay.py b/src/overlay.py new file mode 100644 index 0000000..67fece6 --- /dev/null +++ b/src/overlay.py @@ -0,0 +1,123 @@ +import tkinter as tk +import warframe_api as warpy +import win32gui +import win32api +import win32con +import requests +from ctypes import windll + +GWL_EXSTYLE = -20 +WS_EX_APPWINDOW = 0x00040000 +WS_EX_TOOLWINDOW = 0x00000080 + +X_POS_WIN_OFFSET = 8 +Y_POS_WIN_OFFSET = 31 + +# DEFINE BASE TK WINDOW SETTINGS +root = tk.Tk() +root.title("TennoUI") +root.wm_attributes("-topmost", True) # Keep at the top most layer (needs change) +root.wm_attributes("-disabled", True) # Disable interactions with the window +root.wm_attributes("-transparent", '#2e3440') # Used to make window transparent +root.overrideredirect(True) # Remove the title bar + + +def enable_clickthrough(_root): + """ + Sets the app window to be click through. + :param _root: Tkinter parent + :return: + """ + _hwnd = windll.user32.GetParent(_root.winfo_id()) + style = windll.user32.GetWindowLongPtrW(_hwnd, GWL_EXSTYLE) + style |= win32con.WS_EX_TRANSPARENT | win32con.WS_EX_LAYERED + win32gui.SetWindowLong(_hwnd, win32con.GWL_EXSTYLE, style) + + +def disable_clickthrough(_root): + """ + TODO: Properly implement this so it can undo click-through transparency + :param _root: Tkinter parent + :return: + """ + _hwnd = windll.user32.GetParent(_root.winfo_id()) + style = windll.user32.GetWindowLongPtrW(_hwnd, GWL_EXSTYLE) + style |= win32con.WS_EX_COMPOSITED | win32con.WS_EX_LAYERED + win32gui.SetWindowLong(_hwnd, win32con.GWL_EXSTYLE, style) + + +root.after(10, lambda: enable_clickthrough(root)) # Make overlay click through. +# Define the root's geometry based on the geometry of specified application. +# List of Warframe class names: +# - WarframePublicEvolutionGfxD3D12 +# - WarframePublicEvolutionGfxD3D11 (NEED TO TEST THIS VALUE) +hwnd = win32gui.FindWindow("WarframePublicEvolutionGfxD3D12", None) +fullscreen = borderless = False +if hwnd: + # Window rectangle includes title bar which results in a mismatch when applying the overlay. + # Client rectangle provides the correct dimensions but does not provide the x and y positions + # of the window relative to the screen space, hence their retrieval with GetWindowRect(). + x_pos, y_pos, _, _ = win32gui.GetWindowRect(hwnd) + _, _, width, height = win32gui.GetClientRect(hwnd) + + if win32gui.GetWindowPlacement(hwnd)[1] == win32con.SW_SHOWMAXIMIZED: + # Checks to see if the window is fullscreen. + fullscreen = True + if width == win32api.GetSystemMetrics(0) and height == win32api.GetSystemMetrics(1): + # Compares screen space to client dimensions. The window is likely borderless if + # the width and height match the screen space and fullscreen is not detected. + borderless = True + +else: + # Assume fullscreen + x_pos, y_pos, width, height, fullscreen = 0, 0, win32api.GetSystemMetrics(0), win32api.GetSystemMetrics(1), True + +if fullscreen: + # root.wm_attributes("-fullscreen", True) + print("Fullscreen is not supported") # Currently unable to display above fullscreen apps + exit() +elif borderless: + root.geometry('%dx%d+%d+%d' % (width, height, x_pos, y_pos)) + print("Borderless") +else: + # Offset required to properly place the window on-top of client rectangle + root.geometry('%dx%d+%d+%d' % (width, height, x_pos + X_POS_WIN_OFFSET, y_pos + Y_POS_WIN_OFFSET)) + print("Windowed") + +timers = tk.Frame(master=root, bg="#FFFFFF", width=340, height=160, cursor="none") + +warframe = warpy.WarframeAPI("pc", requests.session()) + +timers.grid(row=0, column=0) + +tk.Label(timers, text="Cetus", fg="#FFFFFF").grid(row=0, column=0) +tk.Label(timers, text="Deimos", fg="#FFFFFF").grid(row=0, column=2) +tk.Label(timers, text="Vallis", fg="#FFFFFF").grid(row=0, column=4) + +cetus_timer = tk.Label(timers) +vallis_timer = tk.Label(timers) +cambion_timer = tk.Label(timers) +cetus_timer.grid(row=0, column=1) +cambion_timer.grid(row=0, column=3) +vallis_timer.grid(row=0, column=5) + + +def current_cycles(): + cetus_status = warframe.cetus_status() + vallis_status = warframe.vallis_status() + cambion_status = warframe.cambion_status() + if cetus_status["isDay"]: + cetus_timer.config(text="Day") + else: + cetus_timer.config(text="Night") + if vallis_status["isWarm"]: + vallis_timer.config(text="Warm") + else: + vallis_timer.config(text="Cold") + cambion_timer.config(text=cambion_status["active"].capitalize()) + root.after(300000, current_cycles) + + +current_cycles() + +root.mainloop() diff --git a/src/warframe_api.py b/src/warframe_api.py new file mode 100644 index 0000000..f345b41 --- /dev/null +++ b/src/warframe_api.py @@ -0,0 +1,160 @@ +from requests import request + + +class NonPlatformError(Exception): + pass + + +def catch_status_code(f): + def func(*args, **kwargs): + response = f(*args, **kwargs) + if response.status_code != 200: + response.raise_for_status() + return response.json() + return func + + +class WarframeAPI: + + _platforms = ['pc', 'ps4', 'xb1', 'swi'] + + def __init__(self, platform: str, session: request, language: str = 'en'): + if platform not in self._platforms: + raise NonPlatformError(platform) + self.platform = platform + self.language = language + self.api = 'https://api.warframestat.us/{platform}'.format(platform=self.platform) + self.session = session + + @catch_status_code + def worldstate(self): + response = self.session.get(self.api) + return response + + @catch_status_code + def alerts(self): + response = self.session.get(self.api + "/alerts") + return response + + @catch_status_code + def arbitration(self): + response = self.session.get(self.api + "/arbitration") + return response + + @catch_status_code + def cambion_status(self): + response = self.session.get(self.api + "/cambionCycle") + return response + + @catch_status_code + def cetus_status(self): + response = self.session.get(self.api + "/cetusCycle") + return response + + @catch_status_code + def conclave_challenges(self): + response = self.session.get(self.api + "/cetusCycle") + return response + + @catch_status_code + def construction_progress(self): + response = self.session.get(self.api + "/constructionProgress") + return response + + @catch_status_code + def darvo_deal(self): + response = self.session.get(self.api + "/dailyDeals") + return response + + @catch_status_code + def earth_cycle(self): + response = self.session.get(self.api + "/earthCycle") + return response + + @catch_status_code + def ongoing_events(self): + response = self.session.get(self.api + "/events") + return response + + @catch_status_code + def fissures(self): + response = self.session.get(self.api + "/fissures") + return response + + @catch_status_code + def darvo_flash_sale(self): + response = self.session.get(self.api + "/flashSales") + return response + + @catch_status_code + def global_upgrades(self): + response = self.session.get(self.api + "/globalUpgrades") + return response + + @catch_status_code + def invasions(self): + response = self.session.get(self.api + "/invasions") + return response + + @catch_status_code + def kuva_nodes(self): + response = self.session.get(self.api + "/kuva") + return response + + @catch_status_code + def news(self): + response = self.session.get(self.api + "/news") + return response + + @catch_status_code + def nightwave(self): + response = self.session.get(self.api + "/nightwave") + return response + + @catch_status_code + def persistent_enemy_data(self): + response = self.session.get(self.api + "/persistentEnemies") + return response + + @catch_status_code + def riven_stats(self, query: str = None): + if query: + response = self.session.get(self.api + "/rivens/search/{query}".format(query=query)) + else: + response = self.session.get(self.api + "/rivens") + return response + + @catch_status_code + def sentient_outpost(self): + response = self.session.get(self.api + "/sentientOutposts") + return response + + @catch_status_code + def sanctuary_status(self): + response = self.session.get(self.api + "/simaris") + return response + + @catch_status_code + def sortie(self): + response = self.session.get(self.api + "/sortie") + return response + + @catch_status_code + def syndicate_nodes(self): + response = self.session.get(self.api + "/syndicateMissions") + return response + + @catch_status_code + def worldstate_timestamp(self): + response = self.session.get(self.api + "/timestamp") + return response + + @catch_status_code + def vallis_status(self): + response = self.session.get(self.api + "/vallisCycle") + return response + + @catch_status_code + def void_trader(self): + response = self.session.get(self.api + "/voidTrader") + return response