├── src
├── __init__.py
├── bin
│ ├── __init__.py
│ ├── rpc.py
│ ├── uninstall.py
│ └── setup.py
└── utilities
│ ├── __init__.py
│ ├── rpc
│ ├── assets.py
│ ├── __init__.py
│ ├── logger.py
│ ├── database.py
│ └── presence.py
│ └── cli
│ ├── __init__.py
│ ├── errors.py
│ ├── output.py
│ └── input.py
├── index.py
├── .gitignore
├── requirements.txt
├── assets
└── logo.ico
├── screenshots
├── dark-db.png
├── dark-no-db.png
├── light-db.png
└── light-no-db.png
├── config.py
├── wuthering_waves_rpc.spec
├── wuthering_waves_rpc_uninstall.spec
├── wuthering_waves_rpc_setup.spec
├── LICENSE.md
└── README.md
/src/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/bin/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.py:
--------------------------------------------------------------------------------
1 | import src.bin.setup
2 |
--------------------------------------------------------------------------------
/src/utilities/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .venv
2 | __pycache__
3 | logs
4 | dist
5 | build
6 | .vscode
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pypresence
2 | psutil
3 | pyinstaller
4 | rich
5 | pywin32
6 |
--------------------------------------------------------------------------------
/assets/logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAkre/Wuthering-Waves-RPC/HEAD/assets/logo.ico
--------------------------------------------------------------------------------
/screenshots/dark-db.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAkre/Wuthering-Waves-RPC/HEAD/screenshots/dark-db.png
--------------------------------------------------------------------------------
/screenshots/dark-no-db.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAkre/Wuthering-Waves-RPC/HEAD/screenshots/dark-no-db.png
--------------------------------------------------------------------------------
/screenshots/light-db.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAkre/Wuthering-Waves-RPC/HEAD/screenshots/light-db.png
--------------------------------------------------------------------------------
/src/utilities/rpc/assets.py:
--------------------------------------------------------------------------------
1 | class DiscordAssets:
2 | LARGE_IMAGE = "logo"
3 | SMALL_IMAGE = "logo"
4 |
--------------------------------------------------------------------------------
/screenshots/light-no-db.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xAkre/Wuthering-Waves-RPC/HEAD/screenshots/light-no-db.png
--------------------------------------------------------------------------------
/src/utilities/rpc/__init__.py:
--------------------------------------------------------------------------------
1 | from .assets import DiscordAssets
2 | from .logger import Logger
3 | from .database import (
4 | get_database,
5 | get_game_version,
6 | get_player_region,
7 | get_player_union_level,
8 | )
9 | from .presence import Presence
10 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | from os.path import join
2 | from os import getenv
3 |
4 |
5 | class Config:
6 | MAIN_EXECUTABLE_NAME = "Wuthering Waves RPC.exe"
7 | UNINSTALL_EXECUTABLE_NAME = "Uninstall Wuthering Waves RPC.exe"
8 | APPLICATION_ID = "1243855663210303488"
9 | WUWA_PROCESS_NAME = "Wuthering Waves.exe"
10 |
--------------------------------------------------------------------------------
/src/utilities/cli/__init__.py:
--------------------------------------------------------------------------------
1 | from .output import indent, print_divider
2 | from .errors import fatal_error
3 | from .input import (
4 | get_input,
5 | get_boolean_input,
6 | get_startup_preference,
7 | get_shortcut_preference,
8 | get_database_access_preference,
9 | get_rich_presence_install_location,
10 | get_using_steam_version,
11 | get_wuwa_install_location,
12 | get_promote_preference,
13 | get_keep_running_preference,
14 | get_kuro_games_uid,
15 | )
16 |
--------------------------------------------------------------------------------
/src/utilities/cli/errors.py:
--------------------------------------------------------------------------------
1 | from rich.console import Console
2 | from src.utilities.cli import indent, print_divider
3 |
4 |
5 | def fatal_error(
6 | console: Console, message: str, exception: Exception | None = None
7 | ) -> None:
8 | """
9 | Display a fatal error message and exit the program
10 |
11 | :param console: The console to use for output
12 | :param message: The fatal error message to display
13 | """
14 | print_divider(console, "[red]An Error Occurred[/red]", "red")
15 | console.print(message, style="red")
16 |
17 | if exception is not None:
18 | console.print_exception(show_locals=True)
19 |
20 | console.show_cursor(False)
21 | # For some reason using console.input() here doesn't work, so I'm using input() instead
22 | input(indent("Press Enter to exit..."))
23 | exit(1)
24 |
--------------------------------------------------------------------------------
/src/bin/rpc.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from os.path import exists, join, abspath, dirname, normcase, normpath
3 | from json import loads
4 | from src.utilities.rpc import Presence
5 |
6 | config_path = join(abspath(dirname(sys.executable)), "config/config.json")
7 |
8 | if not exists(config_path):
9 | raise Exception(f"Config file does not exist, {config_path}")
10 |
11 | with open(config_path, "r") as f:
12 | config = loads(f.read())
13 | if normpath(normcase(config["rich_presence_install_location"])) != normpath(
14 | normcase(abspath(dirname(sys.executable)))
15 | ):
16 | raise Exception(
17 | "The rich presence install location in the config file does not match the actual install location. Please update the config file, or setup the RPC again"
18 | )
19 |
20 | presence = Presence(config)
21 | presence.start()
22 |
--------------------------------------------------------------------------------
/wuthering_waves_rpc.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 |
3 |
4 | rpc = Analysis(
5 | ['src/bin/rpc.py'],
6 | pathex=[],
7 | binaries=[],
8 | datas=[],
9 | hiddenimports=[],
10 | hookspath=[],
11 | hooksconfig={},
12 | runtime_hooks=[],
13 | excludes=[],
14 | noarchive=False,
15 | optimize=0,
16 | )
17 |
18 | rpc_pyz = PYZ(rpc.pure)
19 |
20 | rpc_exe = EXE(
21 | rpc_pyz,
22 | rpc.scripts,
23 | rpc.binaries,
24 | rpc.datas,
25 | [],
26 | uac_admin=True,
27 | name='Wuthering Waves RPC',
28 | debug=False,
29 | bootloader_ignore_signals=False,
30 | strip=False,
31 | upx=True,
32 | upx_exclude=[],
33 | runtime_tmpdir=None,
34 | console=False,
35 | disable_windowed_traceback=False,
36 | argv_emulation=False,
37 | target_arch=None,
38 | codesign_identity=None,
39 | entitlements_file=None,
40 | icon=['assets\\logo.ico'],
41 | )
42 |
43 |
--------------------------------------------------------------------------------
/wuthering_waves_rpc_uninstall.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 |
3 |
4 | uninstall = Analysis(
5 | ['src/bin/uninstall.py'],
6 | pathex=[],
7 | binaries=[],
8 | datas=[],
9 | hiddenimports=[],
10 | hookspath=[],
11 | hooksconfig={},
12 | runtime_hooks=[],
13 | excludes=[],
14 | noarchive=False,
15 | optimize=0,
16 | )
17 |
18 | uninstall_pyz = PYZ(uninstall.pure)
19 |
20 | uninstall_exe = EXE(
21 | uninstall_pyz,
22 | uninstall.scripts,
23 | uninstall.binaries,
24 | uninstall.datas,
25 | [],
26 | uac_admin=True,
27 | name='Uninstall Wuthering Waves RPC',
28 | debug=False,
29 | bootloader_ignore_signals=False,
30 | strip=False,
31 | upx=True,
32 | upx_exclude=[],
33 | runtime_tmpdir=None,
34 | console=True,
35 | disable_windowed_traceback=False,
36 | argv_emulation=False,
37 | target_arch=None,
38 | codesign_identity=None,
39 | entitlements_file=None,
40 | icon=['assets\\logo.ico'],
41 | )
42 |
43 |
--------------------------------------------------------------------------------
/wuthering_waves_rpc_setup.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 |
3 |
4 | setup = Analysis(
5 | ['index.py'],
6 | pathex=[],
7 | binaries=[],
8 | datas=[('dist/Wuthering Waves RPC.exe', '.'), ('dist/Uninstall Wuthering Waves RPC.exe', '.'), ('assets/logo.ico', '.')],
9 | hiddenimports=[],
10 | hookspath=[],
11 | hooksconfig={},
12 | runtime_hooks=[],
13 | excludes=[],
14 | noarchive=False,
15 | optimize=0,
16 | )
17 |
18 | setup_pyz = PYZ(setup.pure)
19 |
20 | setup_exe = EXE(
21 | setup_pyz,
22 | setup.scripts,
23 | setup.binaries,
24 | setup.datas,
25 | [],
26 | uac_admin=True,
27 | name='Wuthering Waves RPC Setup',
28 | debug=False,
29 | bootloader_ignore_signals=False,
30 | strip=False,
31 | upx=True,
32 | upx_exclude=[],
33 | runtime_tmpdir=None,
34 | console=True,
35 | disable_windowed_traceback=False,
36 | argv_emulation=False,
37 | target_arch=None,
38 | codesign_identity=None,
39 | entitlements_file=None,
40 | icon=['assets\\logo.ico'],
41 | )
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Dawid Szymaniak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/src/utilities/cli/output.py:
--------------------------------------------------------------------------------
1 | from rich.console import Console
2 |
3 |
4 | # The cli doesn't really look nice when everything is flush against the left side of the terminal,
5 | # so I added this function to indent the text
6 | def indent(*args: str, spaces: int = 4) -> str:
7 | """
8 | Indent the provided strings by the specified number of spaces
9 |
10 | :param args: The strings to indent
11 | :param spaces: The number of spaces to indent the strings by
12 | :return: One string with all the provided strings indented
13 | """
14 | output = ""
15 |
16 | for i, arg in enumerate(args):
17 | output += f"{(' ' * spaces)}{arg}"
18 |
19 | if i != len(args) - 1:
20 | output += "\n"
21 |
22 | return output
23 |
24 |
25 | def print_divider(console: Console, text: str, style: str) -> None:
26 | """
27 | Print a section divider
28 |
29 | :param console: The console to use for output
30 | :param content: The text inside the divider
31 | :param style: The style of the divider
32 | """
33 | console.print("\n")
34 | console.rule(text, style=style)
35 | console.print("\n")
36 |
--------------------------------------------------------------------------------
/src/utilities/rpc/logger.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from os import makedirs
3 | from os.path import join, dirname, abspath
4 | from datetime import datetime
5 |
6 |
7 | class Logger:
8 | """
9 | Handles application logging
10 | """
11 |
12 | log_file_path: str
13 |
14 | def __init__(
15 | self,
16 | log_folder: str = join(abspath(dirname(sys.executable)), "logs"),
17 | ):
18 | """
19 | Create a new logger instance
20 |
21 | :param log_folder: The path to the log folder
22 | """
23 | makedirs(log_folder, exist_ok=True)
24 | self.log_folder = log_folder
25 | self.log_file_path = join(log_folder, "log.txt")
26 |
27 | def error(self, message: str):
28 | """
29 | Log an error message
30 | """
31 | self.write("ERROR", message)
32 | print(f"ERROR: {message}")
33 |
34 | def warning(self, message: str):
35 | """
36 | Log a warning message
37 | """
38 | self.write("WARNING", message)
39 | print(f"WARNING: {message}")
40 |
41 | def info(self, message: str):
42 | """
43 | Log an info message
44 | """
45 | self.write("INFO", message)
46 | print(f"INFO: {message}")
47 |
48 | def write(self, type: str, message: str):
49 | """
50 | Write a message to the log file
51 | """
52 | with open(self.log_file_path, "a") as log_file:
53 | log_file.write(f"[{type}] [{datetime.now()}] {message}\n")
54 |
55 | def clear(self):
56 | """
57 | Clear the log file of all content
58 | """
59 | open(self.log_file_path, "w").close()
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/xAkre/Wuthering-Waves-RPC/blob/master/LICENSE.md)
2 | [](https://github.com/xAkre/Wuthering-Waves-RPC/releases)
3 | [](https://github.com/qwertyquerty/pypresence)
4 | 
5 |
6 | # Wuthering Waves Discord Rich Presence
7 |
8 | Enables Discord Rich Presence for Wuthering Waves
9 |
10 |
11 |

12 |

13 |
14 |
15 |

16 |

17 |
18 |
19 |
20 | ## Table of Contents
21 |
22 |
23 | - Features
24 | - Installing
25 | - Usage
26 | - Building from source
27 | - Issues
28 | - Warning
29 |
30 |
31 | ## Features
32 |
33 | - RPC variants
34 | - Database access variant
35 | - This variant accesses the game's local database to retrieve information about the user's union level and region. You should note, however, that this could violate the game's terms of service, potentially leading to account suspension or banning
36 | - Non-Database access variant
37 | - This variant does not access the game's local database, eliminating the risk of violating the game's terms of service
38 | - Automatic launch on startup
39 | - Allows the RPC application to start automatically when the user logs in, removing the need to manually start the application
40 |
41 | ## Installing
42 |
43 | 1. Download the [latest release](
44 | https://github.com/xAkre/Wuthering-Waves-RPC/releases/latest
45 | )
46 | 2. Run the setup executable
47 | 3. Go through the setup process
48 | 4. You're done!
49 |
50 | You may delete the setup executable after installation
51 |
52 | ## Usage
53 |
54 | 1. Simply run the RPC application like any other program
55 |
56 | ## Building from source
57 |
58 | 1. Clone the repository
59 | 2. Run `pip install -r requirements.txt`
60 | 3. Run `build.bat`
61 | 4. The executable will be located in the `dist/` directory
62 |
63 | # Issues
64 |
65 | If you encounter any issues, please open an issue on the [issues page](https://github.com/xAkre/Wuthering-Waves-RPC/issues)
66 |
67 | ## Warning
68 |
69 | This is a third-party application and is not affiliated with Wuthering Waves or its developers. Should you choose to use the RPC with the database access option, you do so at your own risk
70 |
71 |
--------------------------------------------------------------------------------
/src/utilities/rpc/database.py:
--------------------------------------------------------------------------------
1 | from sqlite3 import Connection, connect
2 | from json import loads
3 | from src.utilities.rpc import Logger
4 |
5 |
6 | def get_database(path: str) -> Connection:
7 | """
8 | Get a connection to the local Wuthering Waves database
9 | """
10 | logger = Logger()
11 |
12 | try:
13 | return connect(path)
14 | except Exception as e:
15 | logger.error(f"An error occurred while connecting to the local database: {e}")
16 |
17 |
18 | def get_player_region(connection: Connection, kuro_games_uid: str) -> str:
19 | """
20 | Get the player's region from the local database
21 |
22 | :param connection: The connection to the local database
23 | :param kuro_games_uid: The player's Kuro Games UID
24 | :return: The player's region as a string or "Unknown" if an error occurred
25 | """
26 | logger = Logger()
27 |
28 | try:
29 | sdk_game_data = _get_sdk_level_data(connection, kuro_games_uid)
30 | return sdk_game_data.get("Region") if sdk_game_data.get("Region") else "Unknown"
31 | except Exception as e:
32 | logger.error(f"An error occurred while fetching the user's region: {e}")
33 | return "Unknown"
34 |
35 |
36 | def get_player_union_level(connection: Connection, kuro_games_uid: str) -> str:
37 | """
38 | Get the player's union level from the local database
39 |
40 | :param connection: The connection to the local database
41 | :param kuro_games_uid: The player's Kuro Games UID
42 | :return: The player's union level as a string or "Unknown" if an error occurred
43 | """
44 | logger = Logger()
45 |
46 | try:
47 | sdk_game_data = _get_sdk_level_data(connection, kuro_games_uid)
48 | return sdk_game_data.get("Level") if sdk_game_data.get("Level") else "Unknown"
49 | except Exception as e:
50 | logger.error(f"An error occurred while fetching the user's union level: {e}")
51 | return "Unknown"
52 |
53 |
54 | def get_game_version(connection: Connection) -> str:
55 | """
56 | Get the game version from the local database
57 |
58 | :param connection: The connection to the local database
59 | :return: The game version as a string or "Unknown" if an error occurred
60 | """
61 | logger = Logger()
62 |
63 | try:
64 | cursor = connection.cursor()
65 | result = cursor.execute(
66 | "SELECT * FROM LocalStorage WHERE key = ?", ("PatchVersion",)
67 | ).fetchone()
68 | version = result[1]
69 | return version if version else "Unknown"
70 | except Exception as e:
71 | logger.error(f"An error occurred while fetching the game version: {e}")
72 | return "Unknown"
73 |
74 |
75 | def _get_sdk_level_data(connection: Connection, kuro_games_uid: str) -> dict:
76 | """
77 | Get the player's sdk level data from the local database. The level data is
78 | stored as follows:
79 |
80 | {
81 | "___MetaType___":"___Map___",
82 | "Content": [
83 | ["535414272", [
84 | {
85 | "Region": "Europe",
86 | "Level": 4
87 | }
88 | ]],
89 | ["536678859", [
90 | {
91 | "Region": "Europe",
92 | "Level": 3
93 | }
94 | ]],
95 | ["536789175", [
96 | {
97 | "Region": "Europe",
98 | "Level": 22
99 | }]
100 | ]
101 | ]
102 | }
103 |
104 | Where the first value in the array is the player's Kuro Games UID and the second
105 | is the player's level data
106 |
107 | :param connection: The connection to the local database
108 | :param kuro_games_uid: The player's Kuro Games UID
109 | :return: The player's sdk level data or None if an error occurred or the Kuro Games UID is not found
110 | """
111 | logger = Logger()
112 |
113 | try:
114 | cursor = connection.cursor()
115 | result = cursor.execute(
116 | "SELECT * FROM LocalStorage WHERE key = ?", ("SdkLevelData",)
117 | ).fetchone()
118 | value = loads(result[1])
119 | content = value.get("Content")
120 |
121 | for entry in content:
122 | if entry[0] == kuro_games_uid:
123 | data = entry[1][0]
124 | return data
125 |
126 | return None
127 | except Exception as e:
128 | logger.error(f"An error occurred while fetching the user's level data: {e}")
129 | return None
130 |
--------------------------------------------------------------------------------
/src/utilities/rpc/presence.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from sqlite3 import Connection
4 | from time import sleep, time
5 |
6 | from psutil import NoSuchProcess, Process, pids
7 | from pypresence import Presence as PyPresence
8 |
9 | from config import Config
10 | from src.utilities.rpc import (
11 | DiscordAssets,
12 | Logger,
13 | get_database,
14 | get_game_version,
15 | get_player_region,
16 | get_player_union_level,
17 | )
18 |
19 |
20 | class Presence:
21 | logger: Logger
22 | local_database: Connection | None
23 | """
24 | Local Wuthering Waves database connection. The database is a sqlite database and
25 | is stored inside the Wuthering Waves game folder at
26 | "{Game Folder}/Client/Saved/LocalStorage" if using the steam version else
27 | "{Game Folder}/Wuthering Waves Game/Client/Saved/LocalStorage"
28 | """
29 | presence: PyPresence
30 |
31 | def __init__(self, config: dict) -> None:
32 | self.config = config
33 | self.logger = Logger()
34 |
35 | self.database_directory = os.path.join(
36 | self.config["wuwa_install_location"],
37 | "Client/Saved/LocalStorage"
38 | if self.config["using_steam_version"]
39 | else "Wuthering Waves Game/Client/Saved/LocalStorage",
40 | )
41 |
42 | # If the user wants to access the database, get the database connection
43 | if self.config["database_access_preference"]:
44 | local_storage = self.get_lastest_database_file(self.database_directory)
45 | self.logger.info(f"Found last modified LocalStorage file: {local_storage}")
46 | if local_storage:
47 | database_path = os.path.join(self.database_directory, local_storage)
48 | self.local_database = get_database(database_path)
49 | else:
50 | self.local_database = None
51 | else:
52 | self.local_database = None
53 |
54 | self.presence = PyPresence(Config.APPLICATION_ID)
55 |
56 | def get_lastest_database_file(self, directory: str):
57 | """
58 | Returns the name of the lastest database file with a '.db' extension
59 | in the specified directory.
60 |
61 | :param directory: The directory to search for the lastest file
62 | :return: The name of the lastest file, or None if no matching file is found
63 | """
64 | pattern = re.compile(r".*\.db$")
65 | highest_union_level = -1
66 | latest_file = None
67 |
68 | self.logger.info(f"Looking for the lastest LocalStorage file in {directory}")
69 | for file in os.listdir(directory):
70 | if latest_file is None:
71 | latest_file = file
72 |
73 | if pattern.match(file):
74 | self.logger.info(f"Found LocalStorage file: {file}")
75 |
76 | connection = get_database(os.path.join(directory, file))
77 | union_level = get_player_union_level(
78 | connection, self.config["kuro_games_uid"]
79 | )
80 |
81 | if union_level == "Unknown":
82 | continue
83 |
84 | if int(union_level) > highest_union_level:
85 | highest_union_level = int(union_level)
86 | latest_file = file
87 |
88 | connection.close()
89 |
90 | return latest_file
91 |
92 | def start(self) -> None:
93 | """
94 | Start the RPC
95 | """
96 | try:
97 | self.logger.clear()
98 |
99 | while True:
100 | try:
101 | self.presence.connect()
102 | break
103 | except Exception as e:
104 | self.logger.info(
105 | f"Discord could not be found installed and running on this machine"
106 | )
107 | sleep(15)
108 |
109 | while not self.wuwa_process_exists():
110 | self.logger.info("Wuthering Waves is not running, waiting...")
111 | sleep(15)
112 |
113 | self.logger.info("Wuthering Waves and Discord are running, starting RPC...")
114 | self.start_time = time()
115 | self.presence.update(start=self.start_time)
116 | self.rpc_loop()
117 | except Exception as e:
118 | self.logger.error(f"An uncaught error occured: {e}")
119 |
120 | def rpc_loop(self) -> None:
121 | """
122 | Loop to keep the RPC running
123 | """
124 | # Loop while Wuthering Waves process is running
125 | while self.wuwa_process_exists():
126 | self.update()
127 | sleep(15)
128 |
129 | if self.config["keep_running_preference"]:
130 | self.presence.close()
131 | while not self.wuwa_process_exists():
132 | self.logger.info(
133 | "Wuthering waves has closed, waiting for it to start again..."
134 | )
135 | sleep(30)
136 | self.start()
137 |
138 | self.logger.info("Wuthering Waves has closed, closing RPC...")
139 | self.presence.close()
140 |
141 | def update(self) -> None:
142 | """
143 | Update RPC presence
144 | """
145 | self.logger.info("Updating RPC presence...")
146 |
147 | # Add a button to the RPC to promote the Rich Presence if the user wants to
148 | buttons = (
149 | [
150 | {
151 | "label": "Want a status like this?",
152 | "url": "https://github.com/xAkre/Wuthering-Waves-RPC",
153 | }
154 | ]
155 | if self.config["promote_preference"]
156 | else None
157 | )
158 |
159 | # Update the RPC with only basic information if the user doesn't want to access the database
160 | if self.local_database is None:
161 | self.presence.update(
162 | start=self.start_time,
163 | details="Exploring SOL-III",
164 | large_image=DiscordAssets.LARGE_IMAGE,
165 | large_text="Wuthering Waves",
166 | buttons=buttons,
167 | )
168 | return
169 |
170 | try:
171 | # Check for the lastest database file
172 | local_storage = self.get_lastest_database_file(self.database_directory)
173 | self.logger.info(f"Found last modified LocalStorage file: {local_storage}")
174 |
175 | if local_storage:
176 | database_path = os.path.join(self.database_directory, local_storage)
177 | try:
178 | self.local_database = get_database(database_path)
179 | except self.local_database.Error as e:
180 | self.logger.error(f"Failed to connect to database: {e}")
181 | self.local_database = None
182 |
183 | region = get_player_region(
184 | self.local_database, self.config["kuro_games_uid"]
185 | )
186 | union_level = get_player_union_level(
187 | self.local_database, self.config["kuro_games_uid"]
188 | )
189 | game_version = get_game_version(self.local_database)
190 | except self.local_database.Error as e:
191 | self.logger.error(f"Failed to retrieve game data: {e}")
192 | region = "Unknown"
193 | union_level = "Unknown"
194 | game_version = "Unknown"
195 |
196 | self.presence.update(
197 | start=self.start_time,
198 | details=f"Union Level {union_level}",
199 | state=f"Region: {region}",
200 | large_image=DiscordAssets.LARGE_IMAGE,
201 | large_text="Wuthering Waves",
202 | small_image=DiscordAssets.SMALL_IMAGE,
203 | # For some reason quotes are automatically added around the game version, and i don't want that
204 | small_text=f"Version: {game_version}".replace('"', ""),
205 | buttons=buttons,
206 | )
207 |
208 | def wuwa_process_exists(self) -> bool:
209 | """
210 | Check whether the Wuthering Waves process is running
211 |
212 | :return: True if the process is running, False otherwise
213 | """
214 | for pid in pids():
215 | try:
216 | if Process(pid).name() == Config.WUWA_PROCESS_NAME:
217 | return True
218 | except NoSuchProcess:
219 | pass
220 |
221 | return False
222 |
--------------------------------------------------------------------------------
/src/bin/uninstall.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import subprocess
3 | import os
4 | import tempfile
5 | from shutil import rmtree
6 | from win32com.client import Dispatch
7 | from os.path import exists, join, abspath, dirname, normcase, normpath, expanduser
8 | from json import loads
9 | from rich.console import Console
10 | from src.utilities.cli import fatal_error, indent, print_divider
11 | from config import Config
12 |
13 | console = Console()
14 |
15 | config_path = join(abspath(dirname(sys.executable)), "config/config.json")
16 |
17 | if not exists(config_path):
18 | fatal_error(
19 | console,
20 | indent(f"Config file does not exist. It should be located at {config_path}"),
21 | )
22 |
23 | with open(config_path, "r") as f:
24 | config = loads(f.read())
25 | if normpath(normcase(config["rich_presence_install_location"])) != normpath(
26 | normcase(abspath(dirname(sys.executable)))
27 | ):
28 | fatal_error(
29 | console,
30 | indent(
31 | "The rich presence install location in the config file does not match the actual install location. Please update the config file, or setup the RPC again"
32 | ),
33 | )
34 |
35 |
36 | def remove_startup_task(console: Console):
37 | """
38 | Remove the startup task that was created during installation
39 |
40 | :param console: The console to use for input and output
41 | """
42 | try:
43 | with console.status(indent("Removing the startup task..."), spinner="dots"):
44 | delete_task_command = [
45 | "schtasks",
46 | "/delete",
47 | "/tn",
48 | "Wuthering Waves RPC",
49 | "/f",
50 | ]
51 | subprocess.run(
52 | delete_task_command,
53 | check=True,
54 | stdout=subprocess.DEVNULL,
55 | stderr=subprocess.DEVNULL,
56 | )
57 | console.print(indent("Startup task removed."), style="green")
58 | except:
59 | console.print(
60 | indent(
61 | "Failed to remove the startup task.",
62 | "Please remove it manually via the Task Manager application",
63 | ),
64 | style="red",
65 | )
66 |
67 |
68 | def get_shortcut_target_path(shortcut_path: str) -> str:
69 | """
70 | Get the target path of a Windows shortcut.
71 |
72 | :param shortcut_path: The path to the shortcut (.lnk) file.
73 | :return: The target path that the shortcut points to.
74 | """
75 | shell = Dispatch("WScript.Shell")
76 | shortcut = shell.CreateShortcut(shortcut_path)
77 | return shortcut.TargetPath
78 |
79 |
80 | def find_shortcuts_pointing_to_exe(exe_path: str) -> list[str]:
81 | """
82 | Find all shortcuts that point to a specified executable.
83 | This function searches the user's desktop and the Start Menu.
84 |
85 | :param executable_path: The path to the executable
86 | :return: A list of shortcut paths that point to the specified executable
87 | """
88 | paths_to_search = [
89 | join(
90 | os.getenv("APPDATA"),
91 | "Microsoft/Windows/Start Menu/Programs",
92 | ),
93 | expanduser("~/Desktop"),
94 | ]
95 |
96 | shortcuts_pointing_to_exe = []
97 |
98 | for path in paths_to_search:
99 | for root, _, files in os.walk(path):
100 | for file in files:
101 | if file.endswith(".lnk"):
102 | shortcut_path = join(root, file)
103 | try:
104 | if get_shortcut_target_path(shortcut_path) == exe_path:
105 | shortcuts_pointing_to_exe.append(shortcut_path)
106 | except Exception as e:
107 | console.print(
108 | indent(f"Error reading shortcut {shortcut_path}: {e}"),
109 | style="yellow",
110 | )
111 |
112 | return shortcuts_pointing_to_exe
113 |
114 |
115 | def remove_shortcuts(console: Console, exe_path: str):
116 | """
117 | Remove shortcuts that point to a specified executable
118 |
119 | :param console: The console to use for input and output
120 | :param exe_path: The path to the executable
121 | """
122 | shortcuts = find_shortcuts_pointing_to_exe(exe_path)
123 | if not shortcuts:
124 | console.print(
125 | indent("No shortcuts pointing to the executable were found."),
126 | style="yellow",
127 | )
128 | return
129 |
130 | console.print(
131 | indent(f"Found {len(shortcuts)} shortcuts pointing to the executable:"),
132 | style="yellow",
133 | )
134 | for shortcut in shortcuts:
135 | console.print(indent(shortcut), style="yellow")
136 |
137 | for shortcut in shortcuts:
138 | try:
139 | os.remove(shortcut)
140 | console.print(indent(f"Removed shortcut {shortcut}"), style="green")
141 | except Exception as e:
142 | console.print(
143 | indent(f"Failed to remove shortcut {shortcut}: {e}"), style="red"
144 | )
145 |
146 |
147 | def delete_program_folder(console: Console):
148 | """
149 | Delete the program folder
150 |
151 | :param console: The console to use for input and output
152 | """
153 | try:
154 | for root, _, files in os.walk(abspath(dirname(sys.executable))):
155 | for file in files:
156 | if normcase(normpath(join(root, file))) == normcase(
157 | normpath(sys.executable)
158 | ):
159 | continue
160 | with console.status(indent(f"Removing {file}..."), spinner="dots"):
161 | os.remove(join(root, file))
162 | console.print(indent(f"File {file} removed"), style="green")
163 |
164 | uninstall_exe_path = abspath(sys.executable)
165 |
166 | console.input(indent("Press Enter to finalize the uninstallation"))
167 |
168 | batch_script = f"""
169 | @echo off
170 | timeout 2
171 | rmdir /s /q "{dirname(uninstall_exe_path)}"
172 | if errorlevel 1 (
173 | echo "Failed to remove the program folder ({dirname(uninstall_exe_path)}). Please remove it manually. You may now close this window"
174 | ) else (
175 | echo "{dirname(uninstall_exe_path)} was removed. Uninstallation complete. You may now close this window"
176 | )
177 | """
178 |
179 | with tempfile.NamedTemporaryFile(
180 | delete=False, suffix=".bat", mode="w"
181 | ) as temp_script:
182 | temp_script.write(batch_script)
183 | temp_script_path = temp_script.name
184 |
185 | subprocess.Popen(["cmd", "/k", "start", "/wait", temp_script_path])
186 | sys.exit(0)
187 | except Exception as e:
188 | fatal_error(
189 | console,
190 | indent(
191 | "Failed to remove the program folder",
192 | ),
193 | e,
194 | )
195 |
196 |
197 | print_divider(console, "Wuthering Waves Rich Presence Uninstaller", "white")
198 | console.input(indent("Press Enter to uninstall the Wuthering Waves Rich Presence..."))
199 |
200 | try:
201 | if config["startup_preference"]:
202 | print_divider(
203 | console,
204 | "[green]Removing Wuthering Waves RPC from Windows Task Scheduler[/green]",
205 | "green",
206 | )
207 | remove_startup_task(console)
208 |
209 | print_divider(
210 | console,
211 | "[green]Removing shortcuts pointing to the main executable[/green]",
212 | "green",
213 | )
214 | console.print(
215 | indent(
216 | "Note that this only searches for shortcuts on your Desktop and in the Start Menu.",
217 | "If you have shortcuts in other locations, you will need to remove them manually",
218 | ),
219 | style="yellow",
220 | )
221 | exe_path = join(abspath(dirname(sys.executable)), Config.MAIN_EXECUTABLE_NAME)
222 | remove_shortcuts(console, exe_path)
223 | print_divider(
224 | console,
225 | "[green]Removing shortcuts pointing to the uninstaller[/green]",
226 | "green",
227 | )
228 | uninstall_exe_path = abspath(sys.executable)
229 | remove_shortcuts(console, uninstall_exe_path)
230 | print_divider(console, "[green]Removing the program folder[/green]", "green")
231 | delete_program_folder(console)
232 | print_divider(console, "[green]Uninstallation complete[/green]", "green")
233 | input(indent("Press Enter to exit..."))
234 | except Exception as e:
235 | fatal_error(
236 | console,
237 | indent(
238 | "An error occurred during uninstallation",
239 | ),
240 | e,
241 | )
242 |
--------------------------------------------------------------------------------
/src/utilities/cli/input.py:
--------------------------------------------------------------------------------
1 | import re
2 | from os import path, listdir, makedirs
3 | from shutil import rmtree
4 | from rich.console import Console
5 | from src.utilities.cli import indent, print_divider, fatal_error
6 |
7 |
8 | def get_boolean_input(console: Console, prompt: str) -> bool:
9 | """
10 | Get a boolean input from the user
11 |
12 | :param console: The console to use for input and output
13 | :param prompt: The prompt to display to the user
14 | :return: The boolean input from the user
15 | """
16 | while True:
17 | user_input = console.input(prompt).strip().lower()
18 |
19 | if user_input in ["y", "yes"]:
20 | return True
21 | elif user_input in ["n", "no"]:
22 | return False
23 | else:
24 | console.print(
25 | indent("\n", "Please enter 'Y' for yes or 'N' for no.", "\n"),
26 | style="red",
27 | )
28 |
29 |
30 | def get_using_steam_version(console: Console) -> bool:
31 | """
32 | Prompt the user as to whether they use the steam version of the game.
33 |
34 | :param console: The console to use for input and output
35 | :return: Whether the user is using the steam version of the game
36 | """
37 |
38 | return get_boolean_input(
39 | console,
40 | indent("Are you using the steam version of the game (Y/N): "),
41 | )
42 |
43 |
44 | def get_wuwa_install_location(console: Console, default_location: str) -> str:
45 | """
46 | Get the Wuthering Waves install location from the user
47 |
48 | :param console: The console to use for input and output
49 | :param default_location: The default install location
50 | :return: The Wuthering Waves install location
51 | """
52 | while True:
53 | wuwa_install_location = console.input(
54 | indent(
55 | f"Where is Wuthering Waves installed?",
56 | f'Leave blank for the default location ("{default_location}"): ',
57 | )
58 | ).strip()
59 |
60 | if wuwa_install_location == "":
61 | return default_location
62 |
63 | if path.exists(wuwa_install_location):
64 | if not path.isdir(wuwa_install_location):
65 | console.print(
66 | indent(
67 | "That path is not a folder. Please enter a valid folder.",
68 | ),
69 | style="red",
70 | )
71 | continue
72 |
73 | return wuwa_install_location
74 |
75 | console.print(
76 | indent(
77 | "That path does not exist. Please enter a valid path.",
78 | ),
79 | style="red",
80 | )
81 |
82 |
83 | def get_database_access_preference(console: Console) -> bool:
84 | """
85 | Get the user's preference for accessing the game's local database
86 |
87 | :param console: The console to use for input and output
88 | :return: The user's preference for accessing the game's local database
89 | """
90 | return get_boolean_input(
91 | console,
92 | indent(
93 | "Would you like the rich presence to get data from the game's local database?",
94 | "Without this selected, union level and region data will not be displayed.",
95 | "[red]If you choose yes, I am not responsible for any issues and/or bans that may arise.[/red] (Y/N): ",
96 | ),
97 | )
98 |
99 |
100 | def get_rich_presence_install_location(console: Console, default_location: str) -> str:
101 | """
102 | Get the rich presence install location from the user
103 |
104 | :param console: The console to use for input and output
105 | :param default_location: The default install location
106 | :return: The rich presence install location
107 | """
108 | while True:
109 | rich_presence_install_location = console.input(
110 | indent(
111 | "Where would you like the rich presence to be installed?",
112 | f'Leave blank for the default location ("{default_location}"): ',
113 | )
114 | ).strip()
115 |
116 | if rich_presence_install_location == "":
117 | rich_presence_install_location = default_location
118 |
119 | if path.exists(rich_presence_install_location):
120 | if not path.isdir(rich_presence_install_location):
121 | console.print(
122 | indent(
123 | "That path is a file. Please enter a valid folder.",
124 | ),
125 | style="red",
126 | )
127 | continue
128 |
129 | if len(listdir(rich_presence_install_location)) == 0:
130 | return rich_presence_install_location
131 |
132 | if get_boolean_input(
133 | console,
134 | indent(
135 | "That folder is not empty. Would you like to clear it? (Y/N): ",
136 | ),
137 | ):
138 | try:
139 | with console.status(
140 | indent("Clearing the folder..."), spinner="dots"
141 | ):
142 | rmtree(rich_presence_install_location)
143 | makedirs(rich_presence_install_location)
144 | console.print(indent("Folder cleared."), style="green")
145 |
146 | return rich_presence_install_location
147 | except Exception as e:
148 | fatal_error(
149 | console,
150 | indent(
151 | f"An error occurred while clearing the folder:",
152 | ),
153 | e,
154 | )
155 | else:
156 | if rich_presence_install_location == default_location:
157 | with console.status(indent("Creating the folder..."), spinner="dots"):
158 | makedirs(rich_presence_install_location)
159 | console.print(indent("Folder created."), style="green")
160 |
161 | return rich_presence_install_location
162 |
163 | while True:
164 | if get_boolean_input(
165 | console,
166 | indent(
167 | "That folder does not exist. Would you like to create it? (Y/N): ",
168 | ),
169 | ):
170 | try:
171 | with console.status(
172 | indent("Creating the folder..."), spinner="dots"
173 | ):
174 | makedirs(rich_presence_install_location)
175 | console.print(indent("Folder created."), style="green")
176 |
177 | return rich_presence_install_location
178 | except Exception as e:
179 | fatal_error(
180 | console,
181 | indent(
182 | f"An error occurred while creating the folder",
183 | ),
184 | e,
185 | )
186 | else:
187 | break
188 |
189 |
190 | def get_startup_preference(console: Console) -> bool:
191 | """
192 | Get the user's preference for starting the rich presence on system startup
193 |
194 | :param console: The console to use for input and output
195 | :return: The user's preference for starting the rich presence on system startup
196 | """
197 | return get_boolean_input(
198 | console,
199 | indent(
200 | "Would you like to start the rich presence on system startup? (Y/N): ",
201 | ),
202 | )
203 |
204 |
205 | def get_shortcut_preference(console: Console) -> bool:
206 | """
207 | Get the user's preference for creating a desktop shortcut
208 |
209 | :param console: The console to use for input and output
210 | :return: The user's preference for creating a desktop shortcut
211 | """
212 | return get_boolean_input(
213 | console,
214 | indent(
215 | "Would you like to create a desktop shortcut for the rich presence? (Y/N): ",
216 | ),
217 | )
218 |
219 |
220 | def get_promote_preference(console: Console) -> bool:
221 | """
222 | Get the user's preference for promoting the rich presence on Discord
223 |
224 | :param console: The console to use for input and output
225 | :return: The user's preference for promoting the rich presence on Discord
226 | """
227 | return get_boolean_input(
228 | console,
229 | indent(
230 | "Would you like to help promote the rich presence on Discord?",
231 | "This will add a button to the rich presence that links to the GitHub repository. (Y/N): ",
232 | ),
233 | )
234 |
235 |
236 | def get_keep_running_preference(console: Console) -> bool:
237 | """
238 | Get the user's preference for keeping the rich presence running after Wuthering Waves is closed
239 |
240 | :param console: The console to use for input and output
241 | :return: The user's preference for keeping the rich presence running in the background
242 | """
243 | return get_boolean_input(
244 | console,
245 | indent(
246 | "Would you like to keep the rich presence running in the background?",
247 | "This will keep the rich presence running after Wuthering Waves is closed,",
248 | "and it will wait for the next launch (Y/N): ",
249 | ),
250 | )
251 |
252 |
253 | def get_kuro_games_uid(console: Console) -> str:
254 | """
255 | Get the Kuro Games UID the user wants to check for
256 |
257 | :param console: The console to use for input and output
258 | :return: The Kuro Games UID the user wants to check for
259 | """
260 | while True:
261 | kuro_games_uid_regex = re.compile(r"^\d+$")
262 | user_input = console.input(
263 | indent(
264 | "Please enter the Kuro Games UID you would like the RPC to check for.",
265 | "Note that this is not the UID you see in the bottom right corner of the game.",
266 | "To get this UID, go to the game's settings and click on the 'Account' tab,",
267 | "then go into the 'User Center' section, and you will see the UID there: ",
268 | )
269 | ).strip()
270 |
271 | if user_input and kuro_games_uid_regex.match(user_input):
272 | return user_input
273 | else:
274 | console.print(
275 | indent("\n", "The Kuro Games UID must only contain numbers", "\n"),
276 | style="red",
277 | )
278 |
279 |
280 | def get_input(console, divider_text, callback) -> any:
281 | """
282 | Get input from the user using the provided callback
283 |
284 | :param console: The console to use for displaying the divider
285 | :param divider_text: The text to display in the divider
286 | :param callback: The callback to call to get the input
287 | """
288 | print_divider(console, divider_text, "white")
289 | return callback()
290 |
--------------------------------------------------------------------------------
/src/bin/setup.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import subprocess
3 | from win32com.client import Dispatch
4 | from os import getenv, path, makedirs
5 | from shutil import copyfile
6 | from json import dumps
7 | from rich.console import Console
8 | from config import Config
9 | from src.utilities.cli import (
10 | indent,
11 | print_divider,
12 | fatal_error,
13 | get_database_access_preference,
14 | get_input,
15 | get_rich_presence_install_location,
16 | get_shortcut_preference,
17 | get_wuwa_install_location,
18 | get_startup_preference,
19 | get_using_steam_version,
20 | get_promote_preference,
21 | get_keep_running_preference,
22 | get_kuro_games_uid,
23 | )
24 |
25 | console = Console()
26 |
27 | DEFAULT_WUWA_INSTALL_LOCATION = r"C:\Wuthering Waves"
28 | DEFAULT_RICH_PRESENCE_INSTALL_LOCATION = (
29 | rf"{getenv('LOCALAPPDATA')}\Wuthering Waves RPC"
30 | )
31 | LARGE_DIVIDER = r"""
32 | .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.
33 | / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / /
34 | `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-'
35 | """
36 | ASCII_ART = r"""
37 | __ __ _ _ _
38 | \ \ / / _| |_| |__ ___ _ __(_)_ __ __ _
39 | \ \ /\ / / | | | __| '_ \ / _ \ '__| | '_ \ / _` |
40 | \ V V /| |_| | |_| | | | __/ | | | | | | (_| |
41 | \_/\_/ \__,_|\__|_| |_|\___|_| |_|_| |_|\__, |
42 | __ __ ____ ____ |___/
43 | \ \ / /_ ___ _____ ___ | _ \| _ \ / ___|
44 | \ \ /\ / / _` \ \ / / _ \/ __| | |_) | |_) | |
45 | \ V V / (_| |\ V / __/\__ \ | _ <| __/| |___
46 | \_/\_/ \__,_| \_/ \___||___/ |_| \_\_| \____|
47 |
48 | """
49 |
50 |
51 | def print_welcome_message(console: Console) -> None:
52 | """
53 | Print a welcome message to the console
54 |
55 | :param console: The console to use for output
56 | """
57 | console.print(
58 | "\n\n",
59 | LARGE_DIVIDER,
60 | ASCII_ART,
61 | LARGE_DIVIDER,
62 | indent(
63 | "\n\n",
64 | "[blue]Thank you for choosing to use Wuthering Waves Discord Rich Presence![/blue]",
65 | "Source code for this program can be found at https://github.com/xAkre/Wuthering-Waves-RPC",
66 | "A star would be appreciated if you found this program useful!",
67 | "Please follow the instructions below to set up the program.",
68 | "[red]Please note that this program is not affiliated with Wuthering Waves or its developers.[/red]",
69 | ),
70 | highlight=False,
71 | )
72 |
73 |
74 | def get_config(console: Console) -> dict:
75 | """
76 | Get the configuration options from the user
77 |
78 | :param console: The console to use for input and output
79 | """
80 | using_steam_version = get_input(
81 | console, "Steam Version", lambda: get_using_steam_version(console)
82 | )
83 | wuwa_install_location = get_input(
84 | console,
85 | "Wuthering Waves Install Location",
86 | lambda: get_wuwa_install_location(console, DEFAULT_WUWA_INSTALL_LOCATION),
87 | )
88 | database_access_preference = get_input(
89 | console,
90 | "Database Access Preference",
91 | lambda: get_database_access_preference(console),
92 | )
93 |
94 | if database_access_preference:
95 | kuro_games_uid = get_input(
96 | console,
97 | "Kuro Games UID",
98 | lambda: get_kuro_games_uid(console),
99 | )
100 |
101 | config = {
102 | "using_steam_version": using_steam_version,
103 | "wuwa_install_location": wuwa_install_location,
104 | "database_access_preference": database_access_preference,
105 | "rich_presence_install_location": get_input(
106 | console,
107 | "Rich Presence Install Location",
108 | lambda: get_rich_presence_install_location(
109 | console, DEFAULT_RICH_PRESENCE_INSTALL_LOCATION
110 | ),
111 | ),
112 | "startup_preference": get_input(
113 | console,
114 | "Launch on Startup Preference",
115 | lambda: get_startup_preference(console),
116 | ),
117 | "keep_running_preference": get_input(
118 | console,
119 | "Keep Running Preference",
120 | lambda: get_keep_running_preference(console),
121 | ),
122 | "shortcut_preference": get_input(
123 | console,
124 | "Create Shortcut Preference",
125 | lambda: get_shortcut_preference(console),
126 | ),
127 | "promote_preference": get_input(
128 | console,
129 | "Promote Preference",
130 | lambda: get_promote_preference(console),
131 | ),
132 | }
133 |
134 | if database_access_preference:
135 | config["kuro_games_uid"] = kuro_games_uid
136 |
137 | return config
138 |
139 |
140 | def create_config_folder(console: Console, config: dict) -> None:
141 | """
142 | Create the config folder in the install location
143 |
144 | :param console: The console to use for output
145 | :param config: The configuration options
146 | """
147 | try:
148 | with console.status(
149 | indent("Creating the config folder in the install location..."),
150 | spinner="dots",
151 | ):
152 | makedirs(path.join(config["rich_presence_install_location"], "config"))
153 | console.print(indent("Config folder created."), style="green")
154 | except Exception as e:
155 | fatal_error(
156 | console, indent(f"An error occurred while creating the config folder"), e
157 | )
158 |
159 |
160 | def write_config_to_file(console: Console, config: dict) -> None:
161 | """
162 | Write the configuration to a file
163 |
164 | :param console: The console to use for output
165 | :param config: The configuration options
166 | """
167 | try:
168 | with open(
169 | path.join(
170 | config["rich_presence_install_location"], "config", "config.json"
171 | ),
172 | "w",
173 | ) as f:
174 | f.write(dumps(config, indent=4))
175 |
176 | console.print(indent("Configuration written to file."), style="green")
177 | except Exception as e:
178 | fatal_error(
179 | console, indent(f"An error occurred while writing the config to a file"), e
180 | )
181 |
182 |
183 | def copy_main_exe_to_install_location(console: Console, config: dict) -> None:
184 | """
185 | Copy the main executable to the install location
186 |
187 | :param console: The console to use for output
188 | :param config: The configuration options
189 | """
190 | try:
191 | with console.status(
192 | indent("Copying the main executable to the install location..."),
193 | spinner="dots",
194 | ):
195 | copyfile(
196 | path.join(sys._MEIPASS, Config.MAIN_EXECUTABLE_NAME),
197 | path.join(
198 | config["rich_presence_install_location"],
199 | Config.MAIN_EXECUTABLE_NAME,
200 | ),
201 | )
202 | console.print(
203 | indent("Main executable copied to install location."), style="green"
204 | )
205 | except Exception as e:
206 | fatal_error(
207 | console,
208 | indent(
209 | f"An error occurred while copying the main executable to the install location",
210 | ),
211 | e,
212 | )
213 |
214 |
215 | def copy_uninstall_exe_to_install_location(console: Console, config: dict) -> None:
216 | """
217 | Copy the uninstall executable to the install location
218 |
219 | :param console: The console to use for output
220 | :param config: The configuration options
221 | """
222 | try:
223 | with console.status(
224 | indent("Copying the uninstall executable to the install location..."),
225 | spinner="dots",
226 | ):
227 | copyfile(
228 | path.join(sys._MEIPASS, Config.UNINSTALL_EXECUTABLE_NAME),
229 | path.join(
230 | config["rich_presence_install_location"],
231 | Config.UNINSTALL_EXECUTABLE_NAME,
232 | ),
233 | )
234 | console.print(
235 | indent("Uninstall executable copied to install location."),
236 | style="green",
237 | )
238 | except Exception as e:
239 | fatal_error(
240 | console,
241 | indent(
242 | f"An error occurred while copying the uninstall executable to the install location",
243 | ),
244 | e,
245 | )
246 |
247 |
248 | def add_exe_to_windows_apps(console: Console, config: dict) -> None:
249 | """
250 | Add the executable to the Windows App list
251 |
252 | :param console: The console to use for output
253 | :param config: The configuration options
254 | """
255 | try:
256 | with console.status(
257 | indent("Adding the executable to the Windows App list..."), spinner="dots"
258 | ):
259 | programs_folder = path.join(
260 | getenv("APPDATA"),
261 | "Microsoft/Windows/Start Menu/Programs",
262 | )
263 |
264 | if not path.exists(programs_folder):
265 | makedirs(programs_folder)
266 |
267 | main_exe_shortcut_path = path.join(
268 | programs_folder, Config.MAIN_EXECUTABLE_NAME.replace(".exe", ".lnk")
269 | )
270 | main_exe_shortcut_target = path.join(
271 | config["rich_presence_install_location"], Config.MAIN_EXECUTABLE_NAME
272 | )
273 |
274 | shell = Dispatch("WScript.Shell")
275 | shortcut = shell.CreateShortcut(main_exe_shortcut_path)
276 | shortcut.TargetPath = main_exe_shortcut_target
277 | shortcut.Save()
278 |
279 | uninstall_exe_shortcut_path = path.join(
280 | programs_folder,
281 | Config.UNINSTALL_EXECUTABLE_NAME.replace(".exe", ".lnk"),
282 | )
283 | uninstall_exe_shortcut_target = path.join(
284 | config["rich_presence_install_location"],
285 | Config.UNINSTALL_EXECUTABLE_NAME,
286 | )
287 |
288 | shortcut = shell.CreateShortcut(uninstall_exe_shortcut_path)
289 | shortcut.TargetPath = uninstall_exe_shortcut_target
290 | shortcut.Save()
291 |
292 | console.print(
293 | indent("Executable added to Windows App list."), style="green"
294 | )
295 | except Exception as e:
296 | console.print(
297 | indent(
298 | "An error occurred while adding the executable to the Windows App list:"
299 | ),
300 | style="red",
301 | )
302 | console.print_exception()
303 | console.print(
304 | indent("The executable will not be added to the Windows App list.")
305 | )
306 | console.print(indent("Setup will continue..."))
307 |
308 |
309 | def launch_exe_on_startup(console: Console, config: dict) -> None:
310 | """
311 | Launch the executable on system startup
312 |
313 | :param console: The console to use for output
314 | :param config: The configuration options
315 | """
316 | try:
317 | with console.status(
318 | indent("Setting the executable to launch on startup..."), spinner="dots"
319 | ):
320 | shortcut_target = path.join(
321 | config["rich_presence_install_location"],
322 | Config.MAIN_EXECUTABLE_NAME,
323 | )
324 |
325 | # Thank god ChatGPT for this
326 | create_task_command = [
327 | "schtasks",
328 | "/create",
329 | "/tn",
330 | "Wuthering Waves RPC",
331 | "/tr",
332 | f'"{shortcut_target}"',
333 | "/sc",
334 | "onlogon",
335 | "/rl",
336 | "highest",
337 | "/f",
338 | ]
339 |
340 | subprocess.run(create_task_command, check=True, stdout=subprocess.DEVNULL)
341 | console.print(indent("Executable set to launch on startup."), style="green")
342 | except Exception as e:
343 | console.print(
344 | indent(
345 | "An error occurred while setting the executable to launch on startup:"
346 | ),
347 | style="red",
348 | )
349 | console.print_exception()
350 | console.print(
351 | indent("The executable will not launch on startup. Setup"), style="red"
352 | )
353 | console.print(indent("Setup will continue..."))
354 |
355 |
356 | def create_windows_shortcut(console: Console, config: dict) -> None:
357 | """
358 | Create a desktop shortcut for the executable
359 |
360 | :param console: The console to use for output
361 | :param config: The configuration options
362 | """
363 | try:
364 | with console.status(indent("Creating a desktop shortcut..."), spinner="dots"):
365 | shortcut_path = path.join(
366 | path.expanduser("~/Desktop"),
367 | Config.MAIN_EXECUTABLE_NAME.replace(".exe", ".lnk"),
368 | )
369 | shortcut_target = path.join(
370 | config["rich_presence_install_location"], Config.MAIN_EXECUTABLE_NAME
371 | )
372 | shell = Dispatch("WScript.Shell")
373 | shortcut = shell.CreateShortcut(shortcut_path)
374 | shortcut.TargetPath = shortcut_target
375 | shortcut.Save()
376 | console.print(indent("Desktop shortcut created."), style="green")
377 | except Exception as e:
378 | console.print(
379 | indent("An error occurred while creating the desktop shortcut:"),
380 | style="red",
381 | )
382 | console.print_exception()
383 | console.print(indent("The desktop shortcut will not be created."), style="red")
384 | console.print(indent("Setup will continue..."))
385 |
386 |
387 | print_welcome_message(console)
388 | config = get_config(console)
389 | print_divider(console, "[green]Options Finalised[/green]", "green")
390 | create_config_folder(console, config)
391 | write_config_to_file(console, config)
392 | copy_main_exe_to_install_location(console, config)
393 | copy_uninstall_exe_to_install_location(console, config)
394 | add_exe_to_windows_apps(console, config)
395 | if config["startup_preference"]:
396 | launch_exe_on_startup(console, config)
397 | if config["shortcut_preference"]:
398 | create_windows_shortcut(console, config)
399 | print_divider(console, "[green]Setup Completed[/green]", "green")
400 | console.show_cursor(False)
401 |
402 | # For some reason using console.input() here doesn't work, so I'm using input() instead
403 | input(indent("Press Enter to exit..."))
404 | exit(0)
405 |
--------------------------------------------------------------------------------