diff --git a/DMMGamePlayerFastLauncher/cli.py b/DMMGamePlayerFastLauncher/cli.py new file mode 100644 index 0000000..d7236e2 --- /dev/null +++ b/DMMGamePlayerFastLauncher/cli.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import logging +import time +import urllib.parse +from enum import Enum +from pathlib import Path +from typing import Optional + +import typer +from lib.DGPSessionBase import DgpSessionBase +from selenium import webdriver +from selenium.webdriver.chrome.options import Options as ChromeOptions +from selenium.webdriver.firefox.options import Options as FirefoxOptions + + +class Browser(str, Enum): + chrome = "chrome" + firefox = "firefox" + + +class GameType(str, Enum): + GCL = "GCL" + ACL = "ACL" + AMAIN = "AMAIN" + GMAIN = "GMAIN" + + +def resolve( + product_id: str = typer.Argument(..., help="DMM product ID."), + game_type: GameType = typer.Option(GameType.GCL, "--game-type", help="DMM game type."), + game_dir: Optional[Path] = typer.Option(None, "--game-dir", help="Game install directory."), + browser: Browser = typer.Option(Browser.chrome, "--browser", help="Browser used for login."), + browser_arg: Optional[list[str]] = typer.Option(None, "--browser-arg", help="Extra browser argument. Can be repeated."), + proxy: Optional[str] = typer.Option(None, "--proxy", help="Proxy URL for DMM API requests."), + log_level: str = typer.Option("WARNING", "--log-level", help="Python logging level."), +): + """Resolve and print only the game executable arguments.""" + logging.basicConfig(level=getattr(logging, log_level.upper(), logging.WARNING), format="[%(levelname)s] %(message)s") + + if proxy: + DgpSessionBase.PROXY["http"] = proxy + DgpSessionBase.PROXY["https"] = proxy + + session = DgpSessionBase() + res = session.post_dgp(session.LOGIN_URL, json={"prompt": ""}, verify=False).json() + if res["result_code"] != 100: + raise RuntimeError(res["error"]) + + if browser == Browser.chrome: + options = ChromeOptions() + for arg in browser_arg or []: + options.add_argument(arg) + driver = webdriver.Chrome(options=options) + else: + options = FirefoxOptions() + for arg in browser_arg or []: + options.add_argument(arg) + driver = webdriver.Firefox(options=options) + + try: + driver.get(res["data"]["url"]) + parsed_url = urllib.parse.urlparse(driver.current_url) + while not (parsed_url.netloc == "webdgp-gameplayer.games.dmm.com" and parsed_url.path == "/login/success"): + time.sleep(0.2) + parsed_url = urllib.parse.urlparse(driver.current_url) + code = urllib.parse.parse_qs(parsed_url.query)["code"][0] + finally: + driver.quit() + + res = session.post_dgp(session.ACCESS_TOKEN, json={"code": code}, verify=False).json() + if res["result_code"] != 100: + raise RuntimeError(res["error"]) + session.actauth = {"accessToken": res["data"]["access_token"]} + + response = session.launch(product_id, game_type.value).json() + if response["result_code"] != 100: + raise RuntimeError(response["error"]) + + if game_dir is not None: + game_dir.mkdir(parents=True, exist_ok=True) + for _ in session.download(response["data"]["sign"], response["data"]["file_list_url"], game_dir): + pass + + typer.echo(response["data"].get("execute_args")) + + +def main(): + typer.run(resolve) + + +if __name__ == "__main__": + main() diff --git a/DMMGamePlayerFastLauncher/lib/DGPSessionBase.py b/DMMGamePlayerFastLauncher/lib/DGPSessionBase.py new file mode 100644 index 0000000..5c0f067 --- /dev/null +++ b/DMMGamePlayerFastLauncher/lib/DGPSessionBase.py @@ -0,0 +1,249 @@ +import concurrent.futures +import hashlib +import json +import logging +import random +from pathlib import Path +from urllib.parse import parse_qsl + +import requests +import requests.cookies +import urllib3 + +urllib3.disable_warnings() + + +def text_factory(x: bytes): + try: + return x.decode("utf-8") + except Exception: + return x + + +class DgpSessionUtils: + @staticmethod + def gen_rand_hex(): + return hashlib.sha256(str(random.random()).encode()).hexdigest() + + @staticmethod + def gen_rand_address(): + hex = DgpSessionUtils.gen_rand_hex() + address = "" + for x in range(12): + address += hex[x] + if x % 2 == 1: + address += ":" + return address[:-1] + + +class DMMAlreadyRunningException(Exception): + pass + + +class DgpSessionBase: + DGP5_DATA_PATH = Path(".") + + HEADERS: dict[str, str] = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", + } + DGP5_HEADERS: dict[str, str] = { + "Connection": "keep-alive", + "User-Agent": "DMMGamePlayer5-Win/5.3.25 Electron/34.3.0", + "Client-App": "DMMGamePlayer5", + "Client-Version": "5.3.25", + "Sec-Fetch-Site": "none", + "Sec-Fetch-Mode": "no-cors", + "Sec-Fetch-Dest": "empty", + "Accept-Encoding": "gzip, deflate, br, zstd", + "Accept-Language": "ja", + "Priority": "u=1, i", + } + DGP5_DEVICE_PARAMS: dict[str, str] = { + "mac_address": DgpSessionUtils.gen_rand_address(), + "hdd_serial": DgpSessionUtils.gen_rand_hex(), + "motherboard": DgpSessionUtils.gen_rand_hex(), + "user_os": "win", + } + DATA_DESCR: str = "DMMGamePlayerFastLauncher" + LOGGER = logging.getLogger("DgpSessionBase") + + API_DGP = "https://apidgp-gameplayer.games.dmm.com{0}" + LAUNCH_CL = API_DGP.format("/v5/r2/launch/cl") + LAUNCH_PKG = API_DGP.format("/v5/launch/pkg") + HARDWARE_CODE = API_DGP.format("/v5/hardwarecode") + HARDWARE_CONF = API_DGP.format("/v5/hardwareconf") + HARDWARE_LIST = API_DGP.format("/v5/hardwarelist") + HARDWARE_REJECT = API_DGP.format("/v5/hardwarereject") + USER_INFO = API_DGP.format("/v5/userinfo") + CHECK_ACCESS_TOKEN = API_DGP.format("/v5/auth/accesstoken/check") + ACCESS_TOKEN = API_DGP.format("/v5/auth/accesstoken/issue") + LOGIN_URL = API_DGP.format("/v5/auth/login/url") + SIGNED_URL = "https://cdn-gameplayer.games.dmm.com/product/*" + WEB_LOGIN_URL = "https://accounts.dmm.com/service/oauth/=/path=" + PROXY = {} + + LAUNCH_GAME_OS = "win" + LAUNCH_TYPE = "LIB" + + actauth: dict[str, str] + session: requests.Session + + def __init__(self): + self.actauth = {} + self.session = requests.Session() + self.session.cookies = requests.cookies.RequestsCookieJar() + self.session.cookies.set("age_check_done", "0", domain=".dmm.com", path="/") + self.session.proxies = self.PROXY + + def get_access_token(self): + return self.actauth.get("accessToken") + + def get_headers(self): + return self.DGP5_HEADERS | {"actauth": self.get_access_token()} + + def get(self, url: str, params=None, **kwargs) -> requests.Response: + self.LOGGER.info("params %s", params) + res = self.session.get(url, headers=self.HEADERS, params=params, **kwargs) + return self.logger(res) + + def post(self, url: str, json=None, **kwargs) -> requests.Response: + self.LOGGER.info("json %s", json) + res = self.session.post(url, headers=self.HEADERS, json=json, **kwargs) + return self.logger(res) + + def get_dgp(self, url: str, params=None, **kwargs) -> requests.Response: + self.LOGGER.info("params %s", params) + res = self.session.get(url, headers=self.get_headers(), params=params, **kwargs) + return self.logger(res) + + def post_dgp(self, url: str, json=None, **kwargs) -> requests.Response: + self.LOGGER.info("json %s", json) + res = self.session.post(url, headers=self.get_headers(), json=json, **kwargs) + return self.logger(res) + + def post_device_dgp(self, url: str, json=None, **kwargs) -> requests.Response: + json = (json or {}) | self.DGP5_DEVICE_PARAMS + return self.post_dgp(url, json=json, **kwargs) + + def logger(self, res: requests.Response) -> requests.Response: + if res.headers.get("Content-Type", "").startswith("application/json"): + self.LOGGER.info("application/json %s", res.text) + return res + + def get_launch_url(self, game_type: str) -> str: + if game_type in ["GCL", "ACL"]: + return self.LAUNCH_CL + if game_type in ["AMAIN", "GMAIN"]: + return self.LAUNCH_PKG + raise Exception("Unknown game_type: " + game_type) + + def get_launch_payload(self, product_id: str, game_type: str) -> dict[str, str]: + return { + "product_id": product_id, + "game_type": game_type, + "game_os": self.LAUNCH_GAME_OS, + "launch_type": self.LAUNCH_TYPE, + } + + def launch(self, product_id: str, game_type: str) -> requests.Response: + url = self.get_launch_url(game_type) + payload = self.get_launch_payload(product_id, game_type) + return self.post_device_dgp(url, json=payload, verify=False) + + def lunch(self, product_id: str, game_type: str) -> requests.Response: + return self.launch(product_id, game_type) + + def download(self, sign: str, filelist_url: str, output: Path): + sign_dict = dict(parse_qsl(sign.replace(";", "&"))) + signed = { + "Policy": sign_dict["CloudFront-Policy"], + "Signature": sign_dict["CloudFront-Signature"], + "Key-Pair-Id": sign_dict["CloudFront-Key-Pair-Id"], + } + url = self.API_DGP.format(filelist_url) + data = self.get_dgp(url).json() + file_list = data["data"]["file_list"] + + def download_save(file: dict) -> tuple[int, dict]: + path = output.joinpath(file["local_path"][1:]) + content = self.get(data["data"]["domain"] + "/" + file["path"], params=signed).content + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "wb") as f: + f.write(content) + return file["size"], file + + def check_sum(file: dict) -> tuple[bool, dict]: + path = output.joinpath(file["local_path"][1:]) + if not file["check_hash_flg"]: + return True, file + if file["force_delete_flg"]: + if path.exists(): + path.unlink() + return True, file + if not path.exists(): + return False, file + try: + with open(path, "rb") as f: + content = f.read() + return file["hash"] == hashlib.md5(content).hexdigest(), file + except Exception: + return False, file + + if len(file_list) == 0: + return + + check_count = 0 + check_failed_list: list[dict] = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool: + tasks = [pool.submit(check_sum, x) for x in file_list] + for task in concurrent.futures.as_completed(tasks): + res, file = task.result() + check_count += 1 + if not res: + check_failed_list.append(file) + yield check_count / len(file_list), file + + yield 0, None + + if len(check_failed_list) == 0: + return + + check_failed_list = sorted(check_failed_list, key=lambda x: x["size"], reverse=True) + download_max_size = sum([x["size"] for x in check_failed_list]) + download_size = 0 + download_count = 0 + + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool: + tasks = [pool.submit(download_save, x) for x in check_failed_list] + for task in concurrent.futures.as_completed(tasks): + size, file = task.result() + download_size += size + download_count += 1 + if download_max_size > 0: + yield download_size / download_max_size, file + else: + yield download_count / len(check_failed_list), file + + def get_config(self): + with open(self.DGP5_DATA_PATH.joinpath("dmmgame.cnf"), "r", encoding="utf-8") as f: + config = f.read() + res = json.loads(config) + self.LOGGER.info("READ dmmgame.cnf %s", res) + return res + + def set_config(self, config): + with open(self.DGP5_DATA_PATH.joinpath("dmmgame.cnf"), "w", encoding="utf-8") as f: + f.write(json.dumps(config, indent=4)) + + def split_encrypted_data(self, encrypted_data: bytes) -> tuple[bytes, bytes, bytes, bytes]: + return ( + encrypted_data[0:3], + encrypted_data[3:15], + encrypted_data[15:-16], + encrypted_data[-16:], + ) + + def join_encrypted_data(self, v10: bytes, nonce: bytes, data: bytes, mac: bytes) -> bytes: + return v10 + nonce + data + mac diff --git a/DMMGamePlayerFastLauncher/lib/DGPSessionV2.py b/DMMGamePlayerFastLauncher/lib/DGPSessionV2.py index 60962c0..bbc6ac7 100644 --- a/DMMGamePlayerFastLauncher/lib/DGPSessionV2.py +++ b/DMMGamePlayerFastLauncher/lib/DGPSessionV2.py @@ -1,106 +1,21 @@ import base64 -import concurrent.futures -import hashlib import json import logging import os -import random from pathlib import Path -from urllib.parse import parse_qsl import psutil -import requests -import requests.cookies -import urllib3 from Crypto.Cipher import AES from Crypto.Random import get_random_bytes +from lib.DGPSessionBase import DMMAlreadyRunningException, DgpSessionBase, DgpSessionUtils, text_factory from win32 import win32crypt -urllib3.disable_warnings() - -def text_factory(x: bytes): - try: - return x.decode("utf-8") - except Exception: - return x - - -class DgpSessionUtils: - @staticmethod - def gen_rand_hex(): - return hashlib.sha256(str(random.random()).encode()).hexdigest() - - @staticmethod - def gen_rand_address(): - hex = DgpSessionUtils.gen_rand_hex() - address = "" - for x in range(12): - address += hex[x] - if x % 2 == 1: - address += ":" - return address[:-1] - - -class DMMAlreadyRunningException(Exception): - pass - - -class DgpSessionV2: +class DgpSessionV2(DgpSessionBase): DGP5_PATH = Path(os.environ["PROGRAMFILES"]).joinpath("DMMGamePlayer") DGP5_DATA_PATH = Path(os.environ["APPDATA"]).joinpath("dmmgameplayer5") - - HEADERS: dict[str, str] = { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", - } - DGP5_HEADERS: dict[str, str] = { - "Connection": "keep-alive", - "User-Agent": "DMMGamePlayer5-Win/5.3.25 Electron/34.3.0", - "Client-App": "DMMGamePlayer5", - "Client-Version": "5.3.25", - "Sec-Fetch-Site": "none", - "Sec-Fetch-Mode": "no-cors", - "Sec-Fetch-Dest": "empty", - "Accept-Encoding": "gzip, deflate, br, zstd", - "Accept-Language": "ja", - "Priority": "u=1, i", - } - DGP5_DEVICE_PARAMS: dict[str, str] = { - "mac_address": DgpSessionUtils.gen_rand_address(), - "hdd_serial": DgpSessionUtils.gen_rand_hex(), - "motherboard": DgpSessionUtils.gen_rand_hex(), - "user_os": "win", - } - DATA_DESCR: str = "DMMGamePlayerFastLauncher" LOGGER = logging.getLogger("DgpSessionV2") - API_DGP = "https://apidgp-gameplayer.games.dmm.com{0}" - LAUNCH_CL = API_DGP.format("/v5/r2/launch/cl") - LAUNCH_PKG = API_DGP.format("/v5/launch/pkg") - HARDWARE_CODE = API_DGP.format("/v5/hardwarecode") - HARDWARE_CONF = API_DGP.format("/v5/hardwareconf") - HARDWARE_LIST = API_DGP.format("/v5/hardwarelist") - HARDWARE_REJECT = API_DGP.format("/v5/hardwarereject") - USER_INFO = API_DGP.format("/v5/userinfo") - CHECK_ACCESS_TOKEN = API_DGP.format("/v5/auth/accesstoken/check") - ACCESS_TOKEN = API_DGP.format("/v5/auth/accesstoken/issue") - LOGIN_URL = API_DGP.format("/v5/auth/login/url") - SIGNED_URL = "https://cdn-gameplayer.games.dmm.com/product/*" - WEB_LOGIN_URL = "https://accounts.dmm.com/service/oauth/=/path=" - PROXY = {} - - actauth: dict[str, str] - session: requests.Session - - def __init__(self): - self.actauth = {} - self.session = requests.Session() - self.session.cookies = requests.cookies.RequestsCookieJar() - self.session.cookies.set("age_check_done", "0", domain=".dmm.com", path="/") - self.session.proxies = self.PROXY - def write_safe(self, data: bytes): file = self.DGP5_DATA_PATH.joinpath("authAccessTokenData.enc") with open(file, "wb") as f: @@ -148,129 +63,6 @@ def read_bytes(self, file: str): _, contents = win32crypt.CryptUnprotectData(data) self.actauth = json.loads(contents.decode()) - def get_access_token(self): - return self.actauth.get("accessToken") - - def get_headers(self): - return self.DGP5_HEADERS | {"actauth": self.get_access_token()} - - def get(self, url: str, params=None, **kwargs) -> requests.Response: - self.LOGGER.info("params %s", params) - res = self.session.get(url, headers=self.HEADERS, params=params, **kwargs) - return self.logger(res) - - def post(self, url: str, json=None, **kwargs) -> requests.Response: - self.LOGGER.info("json %s", json) - res = self.session.post(url, headers=self.HEADERS, json=json, **kwargs) - return self.logger(res) - - def get_dgp(self, url: str, params=None, **kwargs) -> requests.Response: - self.LOGGER.info("params %s", params) - res = self.session.get(url, headers=self.get_headers(), params=params, **kwargs) - return self.logger(res) - - def post_dgp(self, url: str, json=None, **kwargs) -> requests.Response: - self.LOGGER.info("json %s", json) - res = self.session.post(url, headers=self.get_headers(), json=json, **kwargs) - return self.logger(res) - - def post_device_dgp(self, url: str, json=None, **kwargs) -> requests.Response: - json = (json or {}) | self.DGP5_DEVICE_PARAMS - return self.post_dgp(url, json=json, **kwargs) - - def logger(self, res: requests.Response) -> requests.Response: - if res.headers.get("Content-Type") == "application/json": - self.LOGGER.info("application/json %s", res.text) - return res - - def lunch(self, product_id: str, game_type: str) -> requests.Response: - if game_type == "GCL": - url = self.LAUNCH_CL - elif game_type == "ACL": - url = self.LAUNCH_CL - elif game_type == "AMAIN": - url = self.LAUNCH_PKG - elif game_type == "GMAIN": - url = self.LAUNCH_PKG - else: - raise Exception("Unknown game_type: " + game_type + " " + product_id) - json = { - "product_id": product_id, - "game_type": game_type, - "game_os": "win", - "launch_type": "LIB", - } - return self.post_device_dgp(url, json=json, verify=False) - - def download(self, sign: str, filelist_url: str, output: Path): - sign_dict = dict(parse_qsl(sign.replace(";", "&"))) - signed = { - "Policy": sign_dict["CloudFront-Policy"], - "Signature": sign_dict["CloudFront-Signature"], - "Key-Pair-Id": sign_dict["CloudFront-Key-Pair-Id"], - } - url = self.API_DGP.format(filelist_url) - data = self.get_dgp(url).json() - - def download_save(file: dict) -> tuple[int, dict]: - path = output.joinpath(file["local_path"][1:]) - content = self.get(data["data"]["domain"] + "/" + file["path"], params=signed).content - path.parent.mkdir(parents=True, exist_ok=True) - with open(path, "wb") as f: - f.write(content) - return file["size"], file - - def check_sum(file: dict) -> tuple[bool, dict]: - path = output.joinpath(file["local_path"][1:]) - if not file["check_hash_flg"]: - return True, file - if file["force_delete_flg"]: - path.unlink() - return True, file - if not path.exists(): - return False, file - try: - with open(path, "rb") as f: - content = f.read() - return file["hash"] == hashlib.md5(content).hexdigest(), file - except Exception: - return False, file - - check_count = 0 - check_failed_list: list[dict] = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool: - tasks = [pool.submit(lambda x: check_sum(x), x) for x in data["data"]["file_list"]] - for task in concurrent.futures.as_completed(tasks): - res, file = task.result() - check_count += 1 - if not res: - check_failed_list.append(file) - yield check_count / len(data["data"]["file_list"]), file - - yield 0, None - - check_failed_list = sorted(check_failed_list, key=lambda x: x["size"], reverse=True) - download_max_size = sum([x["size"] for x in check_failed_list]) - download_size: int = 0 - - with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool: - tasks = [pool.submit(lambda x: download_save(x), x) for x in check_failed_list] - for task in concurrent.futures.as_completed(tasks): - size, file = task.result() - download_size += size - yield download_size / download_max_size, file - - def get_config(self): - with open(self.DGP5_DATA_PATH.joinpath("dmmgame.cnf"), "r", encoding="utf-8") as f: - config = f.read() - res = json.loads(config) - self.LOGGER.info("READ dmmgame.cnf %s", res) - return res - - def set_config(self, config): - with open(self.DGP5_DATA_PATH.joinpath("dmmgame.cnf"), "w", encoding="utf-8") as f: - f.write(json.dumps(config, indent=4)) - def get_aes_key(self): with open(self.DGP5_DATA_PATH.joinpath("Local State"), "r", encoding="utf-8") as f: local_state = json.load(f) @@ -278,34 +70,23 @@ def get_aes_key(self): key = win32crypt.CryptUnprotectData(encrypted_key, None, None, None, 0)[1] return key - def split_encrypted_data(self, encrypted_data: bytes) -> tuple[bytes, bytes, bytes, bytes]: - return ( - encrypted_data[0:3], - encrypted_data[3:15], - encrypted_data[15:-16], - encrypted_data[-16:], - ) - - def join_encrypted_data(self, v10: bytes, nonce: bytes, data: bytes, mac: bytes) -> bytes: - return v10 + nonce + data + mac - - @staticmethod - def read_dgp() -> "DgpSessionV2": - session = DgpSessionV2() + @classmethod + def read_dgp(cls) -> "DgpSessionV2": + session = cls() session.read() return session - @staticmethod - def read_cookies(path: Path) -> "DgpSessionV2": - session = DgpSessionV2() + @classmethod + def read_cookies(cls, path: Path) -> "DgpSessionV2": + session = cls() session.read_bytes(str(path)) return session - @staticmethod - def is_running_dmm() -> bool: + @classmethod + def is_running_dmm(cls) -> bool: for proc in psutil.process_iter(): try: - if Path(proc.exe()) == DgpSessionV2.DGP5_PATH.joinpath("DMMGamePlayer.exe"): + if Path(proc.exe()) == cls.DGP5_PATH.joinpath("DMMGamePlayer.exe"): return True except Exception: pass diff --git a/DMMGamePlayerFastLauncher/lib/process_manager.py b/DMMGamePlayerFastLauncher/lib/process_manager.py index 95b3f02..a6a3ac1 100644 --- a/DMMGamePlayerFastLauncher/lib/process_manager.py +++ b/DMMGamePlayerFastLauncher/lib/process_manager.py @@ -1,4 +1,5 @@ import ctypes +import functools import logging import os import subprocess @@ -50,14 +51,11 @@ 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()] + # Fetch pid + exe in a single process_iter pass. attrs= populates `.info` + # and yields None for exe when access is denied (same result as the old + # per-process .exe() try/except), but avoids one syscall per process. + self.process = [(p.info["pid"], p.info["exe"]) for p in psutil.process_iter(attrs=["pid", "exe"])] else: self.process = _process @@ -88,7 +86,10 @@ def search_or_none(self, name: str) -> Optional[int]: return process[0] +@functools.lru_cache(maxsize=1) def get_sid() -> str: + # The current user's SID is constant for the process lifetime; cache the + # win32 lookup so repeated Schtasks operations don't re-query it. username = os.getlogin() sid, domain, type = win32security.LookupAccountName("", username) sidstr = win32security.ConvertSidToStringSid(sid) diff --git a/DMMGamePlayerFastLauncher/lib/version.py b/DMMGamePlayerFastLauncher/lib/version.py index c5d705e..e8df832 100644 --- a/DMMGamePlayerFastLauncher/lib/version.py +++ b/DMMGamePlayerFastLauncher/lib/version.py @@ -17,13 +17,13 @@ def __ne__(self, other: "Version"): return not self.__eq__(other) def __lt__(self, other: "Version"): - return self.major < other.major or self.minor < other.minor or self.patch < other.patch + return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch) def __le__(self, other: "Version"): return self.__eq__(other) or self.__lt__(other) def __gt__(self, other: "Version"): - return self.major > other.major or self.minor > other.minor or self.patch > other.patch + return (self.major, self.minor, self.patch) > (other.major, other.minor, other.patch) def __ge__(self, other: "Version"): return self.__eq__(other) or self.__gt__(other) diff --git a/assets/i18n/app.en_US.yml b/assets/i18n/app.en_US.yml index 0fac148..f2afe94 100644 --- a/assets/i18n/app.en_US.yml +++ b/assets/i18n/app.en_US.yml @@ -42,7 +42,7 @@ en_US: account_path: Select Account account_path_tooltip: |- Please select the account you created in the 'Account' tab. - always_extract_from_dmm: Always extract from DMM + always_extract_from_dmm: Always extract from DMM (Deprecated) game_args: Game Arguments game_args_tooltip: |- Specify the arguments to pass to the game. diff --git a/assets/i18n/app.ja_JP.yml b/assets/i18n/app.ja_JP.yml index 175a147..a3944a5 100644 --- a/assets/i18n/app.ja_JP.yml +++ b/assets/i18n/app.ja_JP.yml @@ -41,7 +41,7 @@ ja_JP: account_path: アカウントの選択 account_path_tooltip: 「アカウント」タブで作成したアカウントを選択してください。 - always_extract_from_dmm: 常にDMMから抽出する + always_extract_from_dmm: 常にDMMから抽出する(非推奨) game_args: ゲームの引数 game_args_tooltip: |- ゲームに渡す引数を指定します。 diff --git a/assets/i18n/app.zh_CN.yml b/assets/i18n/app.zh_CN.yml index 0604b15..1a6aef7 100644 --- a/assets/i18n/app.zh_CN.yml +++ b/assets/i18n/app.zh_CN.yml @@ -41,7 +41,7 @@ zh_CN: account_path: 选择帐户 account_path_tooltip: |- 请在“帐户”选项卡中选择您创建的帐户。 - always_extract_from_dmm: 始终从DMM提取 + always_extract_from_dmm: 始终从DMM提取(已弃用) game_args: 游戏参数 game_args_tooltip: |- 指定传递给游戏的参数。 diff --git a/assets/i18n/app.zh_TW.yml b/assets/i18n/app.zh_TW.yml index 83379d5..945c64a 100644 --- a/assets/i18n/app.zh_TW.yml +++ b/assets/i18n/app.zh_TW.yml @@ -42,7 +42,7 @@ zh_TW: account_path: 選擇帳戶 account_path_tooltip: |- 請在「帳戶」選項卡中選擇您創建的帳戶。 - always_extract_from_dmm: 總是從DMM提取 + always_extract_from_dmm: 總是從DMM提取(已棄用) game_args: 遊戲引數 game_args_tooltip: |- 指定要傳遞給遊戲的引數。 diff --git a/docs/README-advance.md b/docs/README-advance.md index 8fb69c0..1345332 100644 --- a/docs/README-advance.md +++ b/docs/README-advance.md @@ -16,20 +16,15 @@ ### 簡単な使用法 1. DMMGamePlayerFastLauncher を起動します。 -2. `ショートカット` 内の `ショートカットの作成` を開きます。 -3. `ファイル名` は適当な名前を入力します。 -4. `product_idの選択` は高速起動したいゲームの ID を選択します。 -5. `アカウントの選択` は `常にDMMから抽出する` を選択します。 -6. `UAC自動昇格のショートカットを作成して設定を保存する` をクリックします。 -7. デスクトップに作成されたショートカットをダブルクリックしてゲームを起動します。 - -### 複数アカウントの管理 - -#### アカウントをブラウザからインポートをする - -1. DMMGamePlayerFastLauncher の `アカウント` 内の `ブラウザからインポート` を開きます。 -2. `ファイル名` に任意の名前を付けます。 -3. 好みのブラウザを選択してログインを行いインポートをします。 +1. `アカウント` 内の `ブラウザからインポート` を開きます。 +1. `ファイル名` に任意の名前を付けます。 +1. 好みのブラウザを選択してログインを行いインポートをします。 +1. `ショートカット` 内の `ショートカットの作成` を開きます。 +1. `ファイル名` は適当な名前を入力します。 +1. `product_idの選択` は高速起動したいゲームの ID を選択します。 +1. `アカウントの選択` は `ブラウザからインポート` で作成したアカウントを選択します。 +1. `UAC自動昇格のショートカットを作成して設定を保存する` をクリックします。 +1. デスクトップに作成されたショートカットをダブルクリックしてゲームを起動します。 ### 動作が不安定な場合 diff --git a/requirements.cli.txt b/requirements.cli.txt new file mode 100644 index 0000000..be21bfc --- /dev/null +++ b/requirements.cli.txt @@ -0,0 +1,5 @@ +requests +selenium +tqdm +typer +urllib3