diff --git a/.vscode/settings.json b/.vscode/settings.json index 1cf313c..d341aa2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,7 @@ "python.analysis.typeCheckingMode": "basic", "editor.codeActionsOnSave": { "source.organizeImports": "explicit", - "source.fixAll": "explicit", + "source.fixAll": "explicit" }, "files.exclude": { "**/__pycache__": true, @@ -14,5 +14,10 @@ // "**/data": true, "**/*.spec": true, "**/.venv": true - } -} \ No newline at end of file + }, + "python.testing.unittestArgs": ["-v", "-p", "test_*.py", "-s", "test"], + "python.testing.cwd": "${workspaceRoot}", + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true, + "discord.enabled": false +} diff --git a/DMMGamePlayerFastLauncher/DMMGamePlayerFastLauncher.py b/DMMGamePlayerFastLauncher/DMMGamePlayerFastLauncher.py index c680cb4..a28b1ee 100644 --- a/DMMGamePlayerFastLauncher/DMMGamePlayerFastLauncher.py +++ b/DMMGamePlayerFastLauncher/DMMGamePlayerFastLauncher.py @@ -9,7 +9,7 @@ from app import App from coloredlogs import ColoredFormatter from component.logger import LoggingHandlerMask, StyleScheme, TkinkerLogger -from launch import GameLauncher, LanchLauncher +from launch import GameLauncher, GameLauncherUac, LanchLauncher from lib.DGPSessionV2 import DgpSessionV2 from models.setting_data import AppConfig from static.config import AssetsPathConfig, DataPathConfig, SchtasksConfig, UrlConfig @@ -115,5 +115,17 @@ def loder(master: LanchLauncher): lanch = GameLauncher(loder).create() lanch.thread(id) lanch.mainloop() + +elif type == "force-user-game": + GameLauncherUac.wait([id, "--type", "kill-game"]) + lanch = GameLauncher(loder).create() + lanch.thread(id, force_non_uac=True) + lanch.mainloop() + +elif type == "kill-game": + lanch = GameLauncher(loder).create() + lanch.thread(id, kill=True) + lanch.mainloop() + else: raise Exception("type error") diff --git a/DMMGamePlayerFastLauncher/component/component.py b/DMMGamePlayerFastLauncher/component/component.py index 8fb5db8..7030c93 100644 --- a/DMMGamePlayerFastLauncher/component/component.py +++ b/DMMGamePlayerFastLauncher/component/component.py @@ -44,7 +44,7 @@ def create(self): def enter_event(self, event): assert self.tooltip is not None - self.frame.place(x=self.winfo_rootx() - self.frame.master.winfo_rootx(), y=self.winfo_rooty() - self.frame.master.winfo_rooty() + 25) + self.frame.place(x=self.winfo_rootx() - self.frame.master.winfo_rootx(), y=self.winfo_rooty() - self.frame.master.winfo_rooty() + 28) def leave_event(self, event): assert self.tooltip is not None @@ -118,6 +118,50 @@ def call(self, cmd): return lambda: cmd(self.variable) +class ButtonComponent(CTkFrame): + frame: CTkFrame + text: str + tooltip: Optional[str] + command: Callable[[], None] + state: str + + def __init__( + self, + master: Frame, + text: str, + command: Callable[[], None], + tooltip: Optional[str] = None, + ) -> None: + super().__init__(master, fg_color="transparent") + self.frame = CTkFrame(self.winfo_toplevel(), fg_color=CTkm.theme["LabelComponent"]["fg_color"]) + self.pack(fill=ctk.X, expand=True) + self.text = text + self.tooltip = tooltip + self.command = command + + def create(self): + button = CTkButton(self, text=self.text, command=self.command) + button.pack(fill=ctk.X, pady=5) + if self.tooltip is not None: + button.bind("", self.enter_event) + button.bind("", self.leave_event) + CTkLabel(self.frame, text=self.tooltip, fg_color=CTkm.theme["LabelComponent"]["fg_color"], justify=ctk.LEFT).pack(padx=5, pady=0) + + return self + + def enter_event(self, event): + assert self.tooltip is not None + self.frame.place(x=self.winfo_rootx() - self.frame.master.winfo_rootx(), y=self.winfo_rooty() - self.frame.master.winfo_rooty() + 35) + + def leave_event(self, event): + assert self.tooltip is not None + self.frame.place_forget() + + def destroy(self): + self.frame.destroy() + return super().destroy() + + class PathComponentBase(EntryComponent): variable: PathVar diff --git a/DMMGamePlayerFastLauncher/launch.py b/DMMGamePlayerFastLauncher/launch.py index 965b84d..d7b9237 100644 --- a/DMMGamePlayerFastLauncher/launch.py +++ b/DMMGamePlayerFastLauncher/launch.py @@ -1,5 +1,8 @@ import json import logging +import subprocess +import sys +import time import traceback from base64 import b64encode from pathlib import Path @@ -7,10 +10,12 @@ import customtkinter as ctk import i18n +import psutil from component.component import CTkProgressWindow from customtkinter import CTk from lib.DGPSessionWrap import DgpSessionWrap -from lib.process_manager import ProcessManager +from lib.discord import start_rich_presence +from lib.process_manager import ProcessIdManager, ProcessManager from lib.thread import threading_wrapper from lib.toast import ErrorWindow from models.setting_data import AppConfig @@ -36,9 +41,9 @@ def create(self): return self @threading_wrapper - def thread(self, id: str): + def thread(self, id: str, kill: bool = False, force_non_uac: bool = False): try: - self.launch(id) + self.launch(id, kill, force_non_uac) self.quit() except Exception as e: if not Env.DEVELOP: @@ -46,7 +51,7 @@ def thread(self, id: str): ErrorWindow(self, str(e), traceback.format_exc(), quit=True).create() raise - def launch(self, id: str): + def launch(self, id: str, kill: bool = False, force_non_uac: bool = False): path = DataPathConfig.SHORTCUT.joinpath(id).with_suffix(".json") with open(path, "r", encoding="utf-8") as f: data = ShortcutData.from_dict(json.load(f)) @@ -69,10 +74,6 @@ def launch(self, id: str): with open(drm_path.absolute(), "w+") as f: f.write(response["data"]["drm_auth_token"]) - if not Env.DEVELOP: - if response["data"]["is_administrator"] and not ProcessManager.admin_check(): - raise Exception(i18n.t("app.launch.admin_error")) - game_file = Path(game["detail"]["path"]) game_path = game_file.joinpath(response["data"]["exec_file_name"]) @@ -87,12 +88,31 @@ def launch(self, id: str): session.set_config(dgp_config) dmm_args = response["data"]["execute_args"].split(" ") + data.game_args.get().split(" ") - - process = ProcessManager.run([str(game_path.relative_to(game_file))] + dmm_args, cwd=str(game_file)) - assert process.stdout is not None - - for line in process.stdout: - logging.debug(decode(line)) + game_path = str(game_path.relative_to(game_file)) + game_full_path = str(game_file.joinpath(game_path)) + is_admin = ProcessManager.admin_check() + if response["data"]["is_administrator"] and (not is_admin) and (not force_non_uac): + pid_manager = ProcessIdManager() + process = ProcessManager.admin_run([game_path] + dmm_args, cwd=str(game_file)) + game_pid = pid_manager.new_process().search(game_full_path) + if data.rich_presence.get(): + start_rich_presence(game_pid, id, response["data"]["title"]) + while psutil.pid_exists(game_pid): + time.sleep(1) + else: + process = ProcessManager.run([game_path] + dmm_args, cwd=str(game_file)) + if kill: + try: + process.wait(2) + except subprocess.TimeoutExpired: + for child in psutil.Process(process.pid).children(recursive=True): + child.kill() + else: + if data.rich_presence.get(): + start_rich_presence(process.pid, id, response["data"]["title"]) + assert process.stdout is not None + for line in process.stdout: + logging.debug(decode(line)) class LanchLauncher(CTk): @@ -153,6 +173,18 @@ def launch(self, id: str): session.write() +class GameLauncherUac(CTk): + @staticmethod + def wait(args: list[str]): + if not ProcessManager.admin_check(): + pid_manager = ProcessIdManager() + ProcessManager.admin_run([sys.executable, *args]) + print(sys.executable) + game_pid = pid_manager.new_process().search(sys.executable) + while psutil.pid_exists(game_pid): + time.sleep(1) + + def decode(s: bytes) -> str: try: return s.decode("utf-8").strip() diff --git a/DMMGamePlayerFastLauncher/lib/discord.py b/DMMGamePlayerFastLauncher/lib/discord.py new file mode 100644 index 0000000..c7a9781 --- /dev/null +++ b/DMMGamePlayerFastLauncher/lib/discord.py @@ -0,0 +1,16 @@ +import time + +import i18n +from pypresence import Presence +from static.config import DiscordConfig + + +def start_rich_presence(pid: int, id: str, title: str): + RPC = Presence(DiscordConfig.CLIENT_ID) + RPC.connect() + RPC.update( + state=i18n.t("app.discord.state", name=title), + pid=pid, + start=int(time.time()), + large_image=f"https://media.games.dmm.com/freegame/client/{id}/200.gif", + ) diff --git a/DMMGamePlayerFastLauncher/lib/process_manager.py b/DMMGamePlayerFastLauncher/lib/process_manager.py index 21cf322..51c0374 100644 --- a/DMMGamePlayerFastLauncher/lib/process_manager.py +++ b/DMMGamePlayerFastLauncher/lib/process_manager.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Optional +import psutil import win32security from static.config import AssetsPathConfig, DataPathConfig, SchtasksConfig from static.env import Env @@ -13,10 +14,10 @@ class ProcessManager: @staticmethod - def admin_run(args: list[str]) -> int: - args = [f'"{arg}"' for arg in args] - logging.info(args) - return ctypes.windll.shell32.ShellExecuteW(None, "runas", args[0], " ".join(args[1:]), None, 1) + def admin_run(args: list[str], cwd: Optional[str] = None) -> int: + file, *args = args + logging.info({"cwd": cwd, "args": args, "file": file}) + return ctypes.windll.shell32.ShellExecuteW(None, "runas", file, " ".join([f"{arg}" for arg in args]), cwd, 1) @staticmethod def admin_check() -> bool: @@ -37,6 +38,49 @@ def run_ps(args: str) -> int: text = f'powershell -Command "{args}"' return subprocess.call(text, shell=True) + @staticmethod + def search_process(name: str) -> psutil.Process: + for process in psutil.process_iter(): + if process.name() == name: + return process + raise Exception(f"Process not found: {name}") + + +class ProcessIdManager: + process: list[tuple[int, Optional[str]]] + + def __init__(self, _process: Optional[list[tuple[int, Optional[str]]]] = None) -> None: + def wrapper(x: psutil.Process) -> Optional[str]: + try: + return x.exe() + except Exception: + return None + + if _process is None: + self.process = [(x.pid, wrapper(x)) for x in psutil.process_iter()] + else: + self.process = _process + + def __sub__(self, other: "ProcessIdManager") -> "ProcessIdManager": + process = [x for x in self.process if x not in other.process] + return ProcessIdManager(process) + + def __add__(self, other: "ProcessIdManager") -> "ProcessIdManager": + process = list(set(self.process + other.process)) + return ProcessIdManager(process) + + def __repr__(self) -> str: + return "\n".join([f"{x[0]}: {x[1]}" for x in self.process]) + "\n" + + def new_process(self) -> "ProcessIdManager": + return ProcessIdManager() - self + + def search(self, name: str) -> int: + process = [x[0] for x in self.process if x[1] == name] + if len(process) != 1: + raise Exception(f"Process not found: {name}") + return process[0] + def get_sid() -> str: desc = win32security.GetFileSecurity(".", win32security.OWNER_SECURITY_INFORMATION) diff --git a/DMMGamePlayerFastLauncher/models/setting_data.py b/DMMGamePlayerFastLauncher/models/setting_data.py index 4ed5d67..89f72f1 100644 --- a/DMMGamePlayerFastLauncher/models/setting_data.py +++ b/DMMGamePlayerFastLauncher/models/setting_data.py @@ -10,7 +10,7 @@ @dataclass class SettingData(VariableBase): - last_version: StringVar = field(default_factory=lambda: StringVar(value="v5.4.0")) # field(default_factory=lambda: StringVar(value=Env.VERSION)) + last_version: StringVar = field(default_factory=lambda: StringVar(value="v0.0.0")) # field(default_factory=lambda: StringVar(value=Env.VERSION)) dmm_game_player_program_folder: PathVar = field(default_factory=lambda: PathVar(value=Env.DEFAULT_DMM_GAME_PLAYER_PROGURAM_FOLDER)) dmm_game_player_data_folder: PathVar = field(default_factory=lambda: PathVar(value=Env.DEFAULT_DMM_GAME_PLAYER_DATA_FOLDER)) proxy_http: StringVar = field(default_factory=StringVar) diff --git a/DMMGamePlayerFastLauncher/models/shortcut_data.py b/DMMGamePlayerFastLauncher/models/shortcut_data.py index 67eb884..12477cc 100644 --- a/DMMGamePlayerFastLauncher/models/shortcut_data.py +++ b/DMMGamePlayerFastLauncher/models/shortcut_data.py @@ -12,6 +12,7 @@ class ShortcutData(VariableBase): game_args: StringVar = field(default_factory=StringVar) auto_update: BooleanVar = field(default_factory=lambda: BooleanVar(value=True)) game_type: StringVar = field(default_factory=lambda: StringVar(value="GCL")) + rich_presence: BooleanVar = field(default_factory=lambda: BooleanVar(value=True)) @dataclass diff --git a/DMMGamePlayerFastLauncher/static/config.py b/DMMGamePlayerFastLauncher/static/config.py index 9a43e74..3a94da5 100644 --- a/DMMGamePlayerFastLauncher/static/config.py +++ b/DMMGamePlayerFastLauncher/static/config.py @@ -39,3 +39,7 @@ class UrlConfig(Dump): class SchtasksConfig(Dump): FILE = "schtasks_v1_{0}_{1}" NAME = "\\Microsoft\\Windows\\DMMGamePlayerFastLauncher\\{0}" + + +class DiscordConfig(Dump): + CLIENT_ID = "1209708526889345075" diff --git a/DMMGamePlayerFastLauncher/static/env.py b/DMMGamePlayerFastLauncher/static/env.py index 7dd7aea..404d291 100644 --- a/DMMGamePlayerFastLauncher/static/env.py +++ b/DMMGamePlayerFastLauncher/static/env.py @@ -8,7 +8,7 @@ class Env(Dump): - VERSION = "v5.4.1" + VERSION = "v5.5.0" RELEASE_VERSION = requests.get(UrlConfig.RELEASE_API).json().get("tag_name", VERSION) DEVELOP: bool = os.environ.get("ENV") == "DEVELOP" diff --git a/DMMGamePlayerFastLauncher/tab/shortcut.py b/DMMGamePlayerFastLauncher/tab/shortcut.py index d57891e..0915411 100644 --- a/DMMGamePlayerFastLauncher/tab/shortcut.py +++ b/DMMGamePlayerFastLauncher/tab/shortcut.py @@ -4,7 +4,7 @@ import customtkinter as ctk import i18n -from component.component import CheckBoxComponent, EntryComponent, LabelComponent, OptionMenuComponent, PaddingComponent +from component.component import ButtonComponent, CheckBoxComponent, EntryComponent, LabelComponent, OptionMenuComponent, PaddingComponent from component.tab_menu import TabMenuComponent from customtkinter import CTkBaseClass, CTkButton, CTkFrame, CTkLabel, CTkOptionMenu, CTkScrollableFrame from lib.DGPSessionWrap import DgpSessionWrap @@ -74,15 +74,22 @@ def create(self): text = i18n.t("app.shortcut.account_path") OptionMenuComponent(self, text=text, tooltip=i18n.t("app.shortcut.account_path_tooltip"), values=self.account_name_list, variable=self.data.account_path).create() - game_args_tooltip = i18n.t("app.shortcut.game_args_tooltip") - EntryComponent(self, text=i18n.t("app.shortcut.game_args"), tooltip=game_args_tooltip, variable=self.data.game_args).create() + text = i18n.t("app.shortcut.game_args") + EntryComponent(self, text=text, tooltip=i18n.t("app.shortcut.game_args_tooltip"), variable=self.data.game_args).create() CheckBoxComponent(self, text=i18n.t("app.shortcut.auto_update"), variable=self.data.auto_update).create() + CheckBoxComponent(self, text=i18n.t("app.shortcut.rich_presence"), variable=self.data.rich_presence).create() PaddingComponent(self, height=5).create() - CTkButton(self, text=i18n.t("app.shortcut.create_bypass_shortcut_and_save"), command=self.bypass_callback).pack(fill=ctk.X, pady=5) - CTkButton(self, text=i18n.t("app.shortcut.create_shortcut_and_save"), command=self.save_callback).pack(fill=ctk.X, pady=5) - CTkButton(self, text=i18n.t("app.shortcut.save_only"), command=self.save_only_callback).pack(fill=ctk.X, pady=5) + + text = i18n.t("app.shortcut.create_bypass_shortcut_and_save") + ButtonComponent(self, text=text, tooltip=i18n.t("app.shortcut.create_bypass_shortcut_and_save_tooltip"), command=self.bypass_callback).create() + text = i18n.t("app.shortcut.create_uac_shortcut_and_save") + ButtonComponent(self, text=text, tooltip=i18n.t("app.shortcut.create_uac_shortcut_and_save_tooltip"), command=self.uac_callback).create() + text = i18n.t("app.shortcut.create_shortcut_and_save") + ButtonComponent(self, text=text, tooltip=i18n.t("app.shortcut.create_shortcut_and_save_tooltip"), command=self.save_callback).create() + text = i18n.t("app.shortcut.save_only") + ButtonComponent(self, text=text, tooltip=i18n.t("app.shortcut.save_only_tooltip"), command=self.save_only_callback).create() return self def save(self): @@ -119,6 +126,24 @@ def bypass_callback(self): DataPathConfig.SHORTCUT.joinpath(self.filename.get()).with_suffix(".json").unlink() raise + @error_toast + def uac_callback(self): + self.save() + try: + try: + name, icon, admin = self.get_game_info() + except Exception: + name, icon = self.filename.get(), None + self.toast.error(i18n.t("app.shortcut.game_info_error")) + + sorce = Env.DESKTOP.joinpath(name).with_suffix(".lnk") + args = [self.filename.get()] + Shortcut().create(sorce=sorce, args=args, icon=icon) + self.toast.info(i18n.t("app.shortcut.save_success")) + except Exception: + DataPathConfig.SHORTCUT.joinpath(self.filename.get()).with_suffix(".json").unlink() + raise + @error_toast def save_callback(self): self.save() diff --git a/README-en.md b/README-en.md index 70462f4..920b02c 100644 --- a/README-en.md +++ b/README-en.md @@ -18,6 +18,8 @@ DMM Game Player Fast Launcher for secure and fast start-up - **Manage multiple accounts** - **Automatic game update** - **Launch DRM protected games** +- **Launch via Steam** +- **Discord Rich Presence** ## Installation diff --git a/README.md b/README.md index e72344d..cf8cf30 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ DMM Game Player のゲームを高速かつセキュアに起動できるラン - **複数アカウントの管理** - **ゲームの自動アップデート** - **DRMで保護されたゲームの起動** +- **Steam 経由での起動** +- **Discord Rich Presence** ## インストール diff --git a/assets/i18n/app.en_US.yml b/assets/i18n/app.en_US.yml index 42cea91..8d6feba 100644 --- a/assets/i18n/app.en_US.yml +++ b/assets/i18n/app.en_US.yml @@ -22,9 +22,9 @@ en_US: shortcut: filename: File Name - filename_tooltip: |- - Please provide any name for this shortcut. - Using non-alphanumeric characters may cause problems. + filename_tooltip: | + Please give this shortcut any name you like. + Using characters other than half-width alphanumeric characters may cause issues. product_id: Select product_id product_id_tooltip: | @@ -44,13 +44,34 @@ en_US: For Unity-based games, you can input something like '-screen-fullscreen 0 -screen-width 1280 -screen-height 720' to launch the game in windowed mode with a resolution of 1280x720. auto_update: Automatically update the game on launch - - create_bypass_shortcut_and_save: Create a UAC auto-elevated shortcut and save settings - create_shortcut_and_save: Create shortcut and save settings - save_only: Save settings only - - add_detail: Create a shortcut and settings for fast launching. - edit_detail: Edit the settings for the fast launch shortcut. + rich_presence: Enable Discord Rich Presence + + create_bypass_shortcut_and_save: Create UAC Auto-Elevation Shortcut and Save Settings + create_bypass_shortcut_and_save_tooltip: | + Create a shortcut for UAC auto-elevation and save settings. + Select this option if the game requires administrator privileges to run. + + create_uac_shortcut_and_save: Create UAC Manual-Elevation Shortcut and Save Settings + create_uac_shortcut_and_save_tooltip: |- + Create and save a shortcut for UAC manual-elevation. + Select this option if the game requires administrator privileges to run. + Since the UAC dialog will appear every time the game is launched, manual elevation is required. + It is executed in the same process, enhancing integration with other software. + + create_shortcut_and_save: Create Shortcut and Save Settings + create_shortcut_and_save_tooltip: | + Create a shortcut and save settings. + Select this option if the game does not require administrator privileges to run. + Selecting this option for a game that requires administrator privileges will result in an error. + + save_only: Save Settings Only + save_only_tooltip: | + Save settings without creating a shortcut. + To launch the saved settings from a shortcut, execute the command line: + `DMMGamePlayerFastLauncher.exe [File Name] --type game`. + + add_detail: Create a shortcut and settings for fast launch. + edit_detail: Edit the settings of the fast launch shortcut. file_select: Select File @@ -179,3 +200,6 @@ en_US: utils: file_exists: That file already exists. + + discord: + state: "Playing %{name}" diff --git a/assets/i18n/app.ja_JP.yml b/assets/i18n/app.ja_JP.yml index 53b7b47..5407c2f 100644 --- a/assets/i18n/app.ja_JP.yml +++ b/assets/i18n/app.ja_JP.yml @@ -24,7 +24,7 @@ ja_JP: filename: ファイル名 filename_tooltip: |- このショートカットに任意の名前を付けてください。 - 半角英数字以外を使用すると問題が発生する可能性があります。 + 半角英数字以外の文字列を使用すると問題が発生する可能性があります。 product_id: product_idの選択 product_id_tooltip: |- @@ -45,11 +45,28 @@ ja_JP: ゲームが起動した際にウィンドウモードで1280x720の解像度で起動します。 auto_update: 起動時にゲームの自動更新をする + rich_presence: Discordのリッチプレゼンスを有効にする create_bypass_shortcut_and_save: UAC自動昇格のショートカットを作成して設定を保存する + create_bypass_shortcut_and_save_tooltip: |- + UAC自動昇格のショートカットを作成して設定を保存します。 + 起動する際に管理者権限が必要なゲームの場合はこちらを選択します。 + create_uac_shortcut_and_save: UAC手動昇格のショートカットを作成して設定を保存する + create_uac_shortcut_and_save_tooltip: |- + UAC手動昇格のショートカットを作成して設定を保存します。 + 起動する際に管理者権限が必要なゲームの場合はこちらを選択します。 + ゲームを起動するたびにUACのダイアログが表示されるため、手動で昇格する必要があります。 + 同一プロセスで実行されるため他のソフトウェアとの連携が強化されます。 create_shortcut_and_save: ショートカットを作成して設定を保存する + create_shortcut_and_save_tooltip: |- + ショートカットを作成して設定を保存します。 + 起動する際に管理者権限が不要なゲームの場合はこちらを選択します。 + 管理者権限が必要なゲームでこのオプションを選択するとエラーが発生します。 save_only: 設定の保存のみ行なう - + save_only_tooltip: |- + ショートカットを作成せずに設定のみ保存します。 + 保存した設定からショートカットを起動する場合は + `DMMGamePlayerFastLauncher.exe [ファイル名] --type game` とコマンドラインで実行します。 add_detail: 高速起動を行なうショートカットと設定を作成します。 edit_detail: 高速起動のショートカットの設定を編集します。 @@ -180,3 +197,6 @@ ja_JP: utils: file_exists: そのファイルは既に存在します。 + + discord: + state: "%{name} をプレイ中" diff --git a/assets/i18n/app.zh_CN.yml b/assets/i18n/app.zh_CN.yml index 8c54e9a..30d11cf 100644 --- a/assets/i18n/app.zh_CN.yml +++ b/assets/i18n/app.zh_CN.yml @@ -22,7 +22,9 @@ zh_CN: shortcut: filename: 文件名 - filename_tooltip: 请为此快捷方式提供任何名称。 + filename_tooltip: | + 请给这个快捷方式任意名称。 + 使用半角英文数字以外的字符可能会导致问题。 product_id: 选择 product_id product_id_tooltip: |- @@ -42,10 +44,31 @@ zh_CN: 对于基于 Unity 的游戏,您可以输入类似 '-screen-fullscreen 0 -screen-width 1280 -screen-height 720' 的参数,以窗口模式启动游戏,分辨率为 1280x720。 auto_update: 启动时自动更新游戏 + rich_presence: 启用 Discord 的丰富存在 + + create_bypass_shortcut_and_save: 创建UAC自动提升的快捷方式并保存设置 + create_bypass_shortcut_and_save_tooltip: | + 创建UAC自动提升的快捷方式并保存设置。 + 如果游戏运行需要管理员权限,请选择此选项。 + + create_uac_shortcut_and_save: 创建UAC手动提升的快捷方式并保存设置 + create_uac_shortcut_and_save_tooltip: |- + 创建并保存UAC手动提升的快捷方式。 + 如果游戏运行需要管理员权限,请选择此选项。 + 由于每次启动游戏都会出现UAC对话框,因此需要手动提升权限。 + 它在同一进程中执行,与其他软件的集成性更好。 - create_bypass_shortcut_and_save: 创建 UAC 自动提升的快捷方式并保存设置 create_shortcut_and_save: 创建快捷方式并保存设置 + create_shortcut_and_save_tooltip: | + 创建快捷方式并保存设置。 + 如果游戏运行不需要管理员权限,请选择此选项。 + 对于需要管理员权限的游戏选择此选项将导致错误。 + save_only: 仅保存设置 + save_only_tooltip: | + 仅保存设置而不创建快捷方式。 + 要从快捷方式启动保存的设置,请执行命令行: + `DMMGamePlayerFastLauncher.exe [文件名] --type game`。 add_detail: 创建快速启动的快捷方式和设置。 edit_detail: 编辑快速启动快捷方式的设置。 @@ -176,3 +199,6 @@ zh_CN: utils: file_exists: 该文件已经存在。 + + discord: + state: "正在玩 %{name}" diff --git a/assets/i18n/app.zh_TW.yml b/assets/i18n/app.zh_TW.yml index ffc025e..c1705e3 100644 --- a/assets/i18n/app.zh_TW.yml +++ b/assets/i18n/app.zh_TW.yml @@ -43,10 +43,31 @@ zh_TW: 對於基於 Unity 的遊戲,您可以輸入像 '-screen-fullscreen 0 -screen-width 1280 -screen-height 720' 這樣的引數,以以 1280x720 的解析度以視窗模式啟動遊戲。 auto_update: 啟動時自動更新遊戲 + rich_presence: 啟用 Discord 的豐富存在 + + create_bypass_shortcut_and_save: 創建UAC自動提升快捷方式並保存設置 + create_bypass_shortcut_and_save_tooltip: | + 創建UAC自動提升的快捷方式並保存設置。 + 如果遊戲運行需要管理員權限,請選擇此選項。 + + create_uac_shortcut_and_save: 創建UAC手動提升快捷方式並保存設置 + create_uac_shortcut_and_save_tooltip: |- + 建立並保存UAC手動提升的快捷方式。 + 如果遊戲運行需要管理員權限,請選擇此選項。 + 由於遊戲啟動時會顯示UAC對話框,因此需要手動提升權限。 + 它在同一進程中執行,與其他軟件的整合性更好。 - create_bypass_shortcut_and_save: 創建 UAC 自動提升的快捷方式並保存設置 create_shortcut_and_save: 創建快捷方式並保存設置 + create_shortcut_and_save_tooltip: | + 創建快捷方式並保存設置。 + 如果遊戲運行不需要管理員權限,請選擇此選項。 + 對於需要管理員權限的遊戲選擇此選項將導致錯誤。 + save_only: 僅保存設置 + save_only_tooltip: | + 僅保存設置而不創建快捷方式。 + 若要從快捷方式啟動保存的設置,請執行命令行: + `DMMGamePlayerFastLauncher.exe [檔名] --type game`。 add_detail: 創建快速啟動的快捷方式和設置。 edit_detail: 編輯快速啟動快捷方式的設置。 @@ -177,3 +198,6 @@ zh_TW: utils: file_exists: 該文件已經存在。 + + discord: + state: "正在玩 %{name}" diff --git a/assets/license/LICENSE b/assets/license/LICENSE index 1e8b6ae..3899cbc 100644 --- a/assets/license/LICENSE +++ b/assets/license/LICENSE @@ -5692,6 +5692,35 @@ POSSIBILITY OF SUCH DAMAGES. +================================================================================ + + pypresence-4.3.0.dist-info + +================================================================================ + +MIT License + +Copyright (c) 2022 qwertyquerty + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + + ================================================================================ pyreadline3-3.4.1.dist-info diff --git a/docs/README-advance.md b/docs/README-advance.md index 5db8605..474400f 100644 --- a/docs/README-advance.md +++ b/docs/README-advance.md @@ -61,16 +61,25 @@ GUI の動作が不安定で上手くショートカットが作成されない場合や高度な自動化を行いたい場合、コマンドラインを使用して動作させることができます。 -コマンドを実行した権限と同じ権限でゲームが実行されます。 -つまりこの方法だけでは、UAC自動昇格が行われないので `RunAs` や [sudo](https://bjansen.github.io/scoop-apps/main/sudo/) と組み合わせて使用する必要があります。 -タスクスケジューラを使用する DMMGamePlayerFastLauncher の解説は [タスクスケジューラ](#タスクスケジューラ) で行います。 - ```ps1 DMMGamePlayerFastLauncher.exe [ID] [--type TYPE] ``` - `ID`: 起動するゲームもしくはアカウントのファイル名。省略するとGUIが起動します。 -- `--type TYPE`: `game` もしくは `launcher` を指定。 +- `--type TYPE`: `game`, `launcher`, `kill-game`, `force-user-game`から選択します。 + +### 詳細な--typeの説明 + +`game` を指定するとゲームを起動します。 + +`launcher` を指定するとランチャーを起動します。 + +`kill-game` を指定するとゲームを起動したあと、直ちに終了します。管理者権限で実行する必要があります。このオプションは主に `force-user-game` の内部で利用されます。 + +`force-user-game` を指定するとゲームを強制的にユーザー権限で実行させます。主にCygames製のゲームにSteamオーバーレイを表示させる際に利用します。`kill-game` と `game` を連続して動作させたような挙動を行います。 + +Steamオーバーレイは管理者権限で実行されているゲームでは表示されません。 +Cygames 製のゲームはユーザー権限でも起動することができますが、1日に1回程度、管理者権限で実行させる必要があります。なので `kill-game` を使用して一時的に管理者権限で起動させます。 例: @@ -81,6 +90,15 @@ DMMGamePlayerFastLauncher.exe priconner --type game DMMGamePlayerFastLauncher.exe Karyl --type launcher ``` +### Steam Overlay + +[#コマンドラインで実行する](#コマンドラインで実行する) を参考にSteam Overlayに登録します。 + +権限の自動昇格は行われません。 +`game` と指定しても起動しますが Cygames製のゲームは `force-user-game` と指定すると安定して起動します。 + +![steam](./img/steam.png) + ## タスクスケジューラ UAC 自動昇格はタスクスケジューラを使用して行われるので安全です。 diff --git a/docs/img/steam.png b/docs/img/steam.png new file mode 100644 index 0000000..089aab0 Binary files /dev/null and b/docs/img/steam.png differ diff --git a/requirements.txt b/requirements.txt index 80847ed..5d92441 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,6 +24,7 @@ psutil==5.9.4 pycryptodome==3.17 pyinstaller @ git+https://github.com/fa0311/pyinstaller@4bbed269a4e759f920c59aec138b88b7570e330f pyinstaller-hooks-contrib==2022.14 +pypresence==4.3.0 pyreadline3==3.4.1 python-dateutil==2.8.2 pytz==2022.7.1 diff --git a/setup.iss b/setup.iss index 733d473..20fa50b 100644 --- a/setup.iss +++ b/setup.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "DMMGamePlayerFastLauncher" -#define MyAppVersion "5.4.1" +#define MyAppVersion "5.5.0" #define MyAppPublisher "yuki" #define MyAppURL "https://github.com/fa0311/DMMGamePlayerFastLauncher" #define MyAppExeName "DMMGamePlayerFastLauncher.exe" diff --git a/test/test_process.py b/test/test_process.py new file mode 100644 index 0000000..370f41c --- /dev/null +++ b/test/test_process.py @@ -0,0 +1,18 @@ +import unittest + +from DMMGamePlayerFastLauncher.lib.process_manager import ProcessManager + + +class TestProcessManager(unittest.TestCase): + def test_admin_check(self): + value = ProcessManager.admin_check() + self.assertFalse(value) + + def test_run(self): + value = ProcessManager.run(["echo", "test"]) + data = value.communicate() + self.assertEqual(data, (b"test\r\n", b"")) + + +if __name__ == "__main__": + unittest.main()